From c1449a16ac30bb2de357a49a80a9e737b1849e18 Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 19 Dec 2021 12:25:13 -0500 Subject: [PATCH] analytics: user profile visit stats - refactored LinkVisit to ResourceVisit - centralized all unique/total view counting into resource service - added visit counting for user public profile - added graph for public profile visits to Pro Dashboard --- app/controllers/dashboard.js | 7 +- app/controllers/home.js | 5 +- app/controllers/link.js | 12 +-- app/models/lib/geo-types.js | 9 ++ app/models/link-visit.js | 35 ------- app/models/resource-visit.js | 29 ++++++ app/models/user.js | 7 +- app/services/dashboard.js | 173 ++++++++++++++++---------------- app/services/link.js | 41 -------- app/services/resource.js | 59 +++++++++-- app/views/dashboard/view.pug | 29 ++++-- client/js/site-app.js | 12 +-- data/patches/link-visit-port.js | 17 ++++ data/patches/user-stats.js | 20 ++++ 14 files changed, 252 insertions(+), 203 deletions(-) delete mode 100644 app/models/link-visit.js create mode 100644 app/models/resource-visit.js create mode 100644 data/patches/link-visit-port.js create mode 100644 data/patches/user-stats.js diff --git a/app/controllers/dashboard.js b/app/controllers/dashboard.js index 8d3d279..8d52a31 100644 --- a/app/controllers/dashboard.js +++ b/app/controllers/dashboard.js @@ -78,11 +78,12 @@ class DashboardController extends SiteController { async getDashboardView (req, res, next) { const { dashboard: dashboardService, link: linkService } = this.dtp.services; try { - res.locals.userVisitStats = await dashboardService.getUserVisitStats(req.user); - res.locals.userCountryStats = await dashboardService.getUserCountryStats(req.user); - res.locals.userCityStats = await dashboardService.getUserCityStats(req.user); + res.locals.profileVisitStats = await dashboardService.getResourceVisitStats('User', req.user._id); + res.locals.linkVisitStats = await dashboardService.getUserVisitStats(req.user); res.locals.userLinks = await linkService.getForUser(req.user); + res.locals.linkCountryStats = await dashboardService.getUserCountryStats(req.user); + res.locals.linkCityStats = await dashboardService.getUserCityStats(req.user); res.render('dashboard/view'); } catch (error) { diff --git a/app/controllers/home.js b/app/controllers/home.js index 41cf99f..1fdfac6 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -54,7 +54,7 @@ class HomeController extends SiteController { } async getPublicProfile (req, res, next) { - const { link: linkService } = this.dtp.services; + const { link: linkService, user: userService } = this.dtp.services; try { this.log.debug('profile request', { url: req.url }); if (!res.locals.userProfile) { @@ -63,6 +63,9 @@ class HomeController extends SiteController { res.locals.currentView = 'public-profile'; res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.links = await linkService.getForUser(res.locals.userProfile, res.locals.pagination); + + await userService.recordProfileView(res.locals.userProfile, req); + res.render('profile/home'); } catch (error) { this.log.error('failed to display landing page', { error }); diff --git a/app/controllers/link.js b/app/controllers/link.js index 8c832b2..ccf0e0b 100644 --- a/app/controllers/link.js +++ b/app/controllers/link.js @@ -66,19 +66,11 @@ class LinkController extends SiteController { } async postCreateLinkVisit (req, res, next) { - const { link: linkService, resource: resourceService } = this.dtp.services; + const { resource: resourceService } = this.dtp.services; try { this.log.info('recording link visit', { link: res.locals.link._id, ip: req.ip }); - /* - * Do these jobs in parallel so the total work gets done faster - */ - const jobs = [ - linkService.recordVisit(res.locals.link, req), - resourceService.recordView(req, 'Link', res.locals.link._id), - ]; - - await Promise.all(jobs); + await resourceService.recordView(req, 'Link', res.locals.link._id); res.redirect(res.locals.link.href); // off you go! } catch (error) { diff --git a/app/models/lib/geo-types.js b/app/models/lib/geo-types.js index fb37572..dd5cdce 100644 --- a/app/models/lib/geo-types.js +++ b/app/models/lib/geo-types.js @@ -10,4 +10,13 @@ const Schema = mongoose.Schema; module.exports.GeoPoint = new Schema({ type: { type: String, enum: ['Point'], default: 'Point', required: true }, coordinates: { type: [Number], required: true }, +}); + +module.exports.GeoIp = new Schema({ + country: { type: String }, + region: { type: String }, + eu: { type: String }, + timezone: { type: String }, + city: { type: String }, + location: { type: module.exports.GeoPoint }, }); \ No newline at end of file diff --git a/app/models/link-visit.js b/app/models/link-visit.js deleted file mode 100644 index 65bf677..0000000 --- a/app/models/link-visit.js +++ /dev/null @@ -1,35 +0,0 @@ -// link-visit.js -// Copyright (C) 2021 Digital Telepresence, LLC -// License: Apache-2.0 - -'use strict'; - -const mongoose = require('mongoose'); -const Schema = mongoose.Schema; - -const { GeoPoint } = require('./lib/geo-types'); - -const LinkVisitSchema = new Schema({ - created: { type: Date, required: true, default: Date.now, index: -1, expires: '7d' }, - link: { type: Schema.ObjectId, required: true, index: 1, ref: 'Link' }, - user: { type: Schema.ObjectId, index: 1, ref: 'User' }, - geoip: { - country: { type: String }, - region: { type: String }, - eu: { type: String }, - timezone: { type: String }, - city: { type: String }, - location: { type: GeoPoint }, - }, -}); - -LinkVisitSchema.index({ - user: 1, -}, { - partialFilterExpression: { - user: { $exists: true }, - }, - name: 'link_visits_for_user', -}); - -module.exports = mongoose.model('LinkVisit', LinkVisitSchema); \ No newline at end of file diff --git a/app/models/resource-visit.js b/app/models/resource-visit.js new file mode 100644 index 0000000..bf41d4c --- /dev/null +++ b/app/models/resource-visit.js @@ -0,0 +1,29 @@ +// resource-visit.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const { GeoIp } = require('./lib/geo-types'); + +const ResourceVisitSchema = new Schema({ + created: { type: Date, required: true, default: Date.now, index: -1, expires: '7d' }, + resourceType: { type: String, enum: ['Link', 'User'], required: true }, + resource: { type: Schema.ObjectId, required: true, index: 1, ref: 'Link' }, + user: { type: Schema.ObjectId, index: 1, ref: 'User' }, + geoip: { type: GeoIp }, +}); + +ResourceVisitSchema.index({ + user: 1, +}, { + partialFilterExpression: { + user: { $exists: true }, + }, + name: 'resource_visits_for_user', +}); + +module.exports = mongoose.model('ResourceVisit', ResourceVisitSchema); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 28d9f42..84aaf40 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -8,6 +8,8 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; +const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats'); + const UserFlagsSchema = new Schema({ isAdmin: { type: Boolean, default: false, required: true }, isModerator: { type: Boolean, default: false, required: true }, @@ -34,10 +36,7 @@ const UserSchema = new Schema({ }, flags: { type: UserFlagsSchema, select: false }, permissions: { type: UserPermissionsSchema, select: false }, - stats: { - profileViewCountTotal: { type: Number, default: 0, required: true }, - profileViewCountUnique: { type: Number, default: 0, required: true }, - } + stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, }); module.exports = mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/services/dashboard.js b/app/services/dashboard.js index aa95ac7..39f09fb 100644 --- a/app/services/dashboard.js +++ b/app/services/dashboard.js @@ -7,7 +7,7 @@ const mongoose = require('mongoose'); const Link = mongoose.model('Link'); -const LinkVisit = mongoose.model('LinkVisit'); +const ResourceVisit = mongoose.model('ResourceVisit'); const moment = require('moment'); @@ -21,16 +21,18 @@ class DashboardService extends SiteService { super(dtp, module.exports); } - /* - * - * USER VISIT STATS - * - */ - async getUserVisitStats (user) { + async getResourceVisitStats (resourceType, resourceId) { + if (!resourceId) { + throw new Error('Invalid resource'); + } + + // this will throw if not a valid ObjectId (or able to become one) + resourceId = mongoose.Types.ObjectId(resourceId); + const { cache: cacheService } = this.dtp.services; let stats; - const cacheKey = `stats:user:${user._id}:visit`; + const cacheKey = `stats:${resourceType.toLowerCase()}:${resourceId.toString()}:visit`; if (DashboardService.CACHE_ENABLED) { stats = await cacheService.getObject(cacheKey); if (stats) { @@ -38,18 +40,15 @@ class DashboardService extends SiteService { } } - this.log.info('getUserVisitStats', 'generating user visit stats report', { userId: user._id }); + this.log.info('generating resource visit stats report', { resourceId }); const END_DATE = new Date(); const START_DATE = moment(END_DATE).subtract(7, 'day').toDate(); - const links = await Link.find({ user: user._id }).lean(); - const linkIds = links.map((link) => link._id); - - stats = await LinkVisit.aggregate([ + stats = await ResourceVisit.aggregate([ { $match: { - link: { $in: linkIds }, + resource: resourceId, $and: [ { created: { $gt: START_DATE } }, { created: { $lt: END_DATE } }, @@ -86,25 +85,25 @@ class DashboardService extends SiteService { }, ]); - const response = { - start: START_DATE, - end: END_DATE, - stats, - }; + const response = { start: START_DATE, end: END_DATE, stats }; await cacheService.setObjectEx(cacheKey, 60 * 5, response); + return response; } /* * - * LINK VISIT STATS + * RESOURCE COUNTRY STATS * */ - async getLinkVisitStats (link) { + async getResourceCountryStats (resourceType, resourceId) { const { cache: cacheService } = this.dtp.services; let stats; - const cacheKey = `stats:link:${link._id}:visit`; + // this will throw if not a valid ObjectId (or able to become one) + resourceId = mongoose.Types.ObjectId(resourceId); + + const cacheKey = `stats:${resourceType.toLowerCase()}:${resourceId.toString()}:country`; if (DashboardService.CACHE_ENABLED) { stats = await cacheService.getObject(cacheKey); if (stats) { @@ -112,15 +111,15 @@ class DashboardService extends SiteService { } } - this.log.info('getLinkVisitStats', 'generating link visit stats report', { linkId: link._id }); + this.log.info('generating resource country stats report', { resourceId }); const END_DATE = new Date(); const START_DATE = moment(END_DATE).subtract(7, 'day').toDate(); - stats = await LinkVisit.aggregate([ + stats = await ResourceVisit.aggregate([ { $match: { - link: link._id, + resource: resourceId, $and: [ { created: { $gt: START_DATE } }, { created: { $lt: END_DATE } }, @@ -130,10 +129,7 @@ class DashboardService extends SiteService { { $group: { _id: { - year: { $year: '$created' }, - month: { $month: '$created' }, - day: { $dayOfMonth: '$created' }, - hour: { $hour: '$created' }, + country: '$geoip.country', }, count: { $sum: 1 }, }, @@ -141,41 +137,37 @@ class DashboardService extends SiteService { { $project: { _id: false, - date: { - $dateFromParts: { - year: '$_id.year', - month: '$_id.month', - day: '$_id.day', - hour: '$_id.hour', - }, - }, + country: '$_id.country', count: '$count', }, }, { - $sort: { date: 1 }, + $sort: { count: -1, country: 1 }, + }, + { + $limit: 10, }, ]); - const response = { - start: START_DATE, - end: END_DATE, - stats, - }; + const response = { start: START_DATE, end: END_DATE, stats }; await cacheService.setObjectEx(cacheKey, 60 * 5, response); + return response; } /* * - * USER COUNTRY STATS + * RESOURCE CITY STATS * */ - async getUserCountryStats (user) { + async getResourceCityStats (resourceType, resourceId) { const { cache: cacheService } = this.dtp.services; let stats; - const cacheKey = `stats:user:${user._id}:country`; + // this will throw if not a valid ObjectId (or able to become one) + resourceId = mongoose.Types.ObjectId(resourceId); + + const cacheKey = `stats:${resourceType.toLowerCase()}:${resourceId.toString()}:city`; if (DashboardService.CACHE_ENABLED) { stats = await cacheService.getObject(cacheKey); if (stats) { @@ -183,18 +175,15 @@ class DashboardService extends SiteService { } } - this.log.info('getUserCountryStats', 'generating user country stats report', { userId: user._id }); + this.log.info('generating resource city stats report', { resourceId }); const END_DATE = new Date(); const START_DATE = moment(END_DATE).subtract(7, 'day').toDate(); - const links = await Link.find({ user: user._id }).lean(); - const linkIds = links.map((link) => link._id); - - stats = await LinkVisit.aggregate([ + stats = await ResourceVisit.aggregate([ { $match: { - link: { $in: linkIds }, + resource: resourceId, $and: [ { created: { $gt: START_DATE } }, { created: { $lt: END_DATE } }, @@ -205,6 +194,8 @@ class DashboardService extends SiteService { $group: { _id: { country: '$geoip.country', + region: '$geoip.region', + city: '$geoip.city', }, count: { $sum: 1 }, }, @@ -213,36 +204,35 @@ class DashboardService extends SiteService { $project: { _id: false, country: '$_id.country', + region: '$_id.region', + city: '$_id.city', count: '$count', }, }, { - $sort: { count: -1, country: 1 }, + $sort: { count: -1, city: 1, region: 1, country: 1 }, }, { $limit: 10, }, ]); - const response = { - start: START_DATE, - end: END_DATE, - stats, - }; + const response = { start: START_DATE, end: END_DATE, stats }; await cacheService.setObjectEx(cacheKey, 60 * 5, response); + return response; } /* * - * LINK COUNTRY STATS + * USER VISIT STATS * */ - async getLinkCountryStats (link) { + async getUserVisitStats (user) { const { cache: cacheService } = this.dtp.services; let stats; - const cacheKey = `stats:link:${link._id}:country`; + const cacheKey = `stats:user-links:${user._id}:visit`; if (DashboardService.CACHE_ENABLED) { stats = await cacheService.getObject(cacheKey); if (stats) { @@ -250,15 +240,18 @@ class DashboardService extends SiteService { } } - this.log.info('getLinkCountryStats', 'generating link country stats report', { linkId: link._id }); + this.log.info('generating user visit stats report', { userId: user._id }); const END_DATE = new Date(); const START_DATE = moment(END_DATE).subtract(7, 'day').toDate(); - stats = await LinkVisit.aggregate([ + const links = await Link.find({ user: user._id }).lean(); + const linkIds = links.map((link) => link._id); + + stats = await ResourceVisit.aggregate([ { $match: { - link: link._id, + resource: { $in: linkIds }, $and: [ { created: { $gt: START_DATE } }, { created: { $lt: END_DATE } }, @@ -268,7 +261,10 @@ class DashboardService extends SiteService { { $group: { _id: { - country: '$geoip.country', + year: { $year: '$created' }, + month: { $month: '$created' }, + day: { $dayOfMonth: '$created' }, + hour: { $hour: '$created' }, }, count: { $sum: 1 }, }, @@ -276,15 +272,19 @@ class DashboardService extends SiteService { { $project: { _id: false, - country: '$_id.country', + date: { + $dateFromParts: { + year: '$_id.year', + month: '$_id.month', + day: '$_id.day', + hour: '$_id.hour', + }, + }, count: '$count', }, }, { - $sort: { count: -1, country: 1 }, - }, - { - $limit: 10, + $sort: { date: 1 }, }, ]); @@ -299,14 +299,14 @@ class DashboardService extends SiteService { /* * - * USER CITY STATS + * USER COUNTRY STATS * */ - async getUserCityStats (user) { + async getUserCountryStats (user) { const { cache: cacheService } = this.dtp.services; let stats; - const cacheKey = `stats:user:${user._id}:city`; + const cacheKey = `stats:user-links:${user._id}:country`; if (DashboardService.CACHE_ENABLED) { stats = await cacheService.getObject(cacheKey); if (stats) { @@ -314,7 +314,7 @@ class DashboardService extends SiteService { } } - this.log.info('getUserCityStats', 'generating user city stats report', { userId: user._id }); + this.log.info('generating user country stats report', { userId: user._id }); const END_DATE = new Date(); const START_DATE = moment(END_DATE).subtract(7, 'day').toDate(); @@ -322,10 +322,10 @@ class DashboardService extends SiteService { const links = await Link.find({ user: user._id }).lean(); const linkIds = links.map((link) => link._id); - stats = await LinkVisit.aggregate([ + stats = await ResourceVisit.aggregate([ { $match: { - link: { $in: linkIds }, + resource: { $in: linkIds }, $and: [ { created: { $gt: START_DATE } }, { created: { $lt: END_DATE } }, @@ -336,8 +336,6 @@ class DashboardService extends SiteService { $group: { _id: { country: '$geoip.country', - region: '$geoip.region', - city: '$geoip.city', }, count: { $sum: 1 }, }, @@ -346,13 +344,11 @@ class DashboardService extends SiteService { $project: { _id: false, country: '$_id.country', - region: '$_id.region', - city: '$_id.city', count: '$count', }, }, { - $sort: { count: -1, city: 1, region: 1, country: 1 }, + $sort: { count: -1, country: 1 }, }, { $limit: 10, @@ -370,14 +366,14 @@ class DashboardService extends SiteService { /* * - * LINK CITY STATS + * USER CITY STATS * */ - async getLinkCityStats (link) { + async getUserCityStats (user) { const { cache: cacheService } = this.dtp.services; let stats; - const cacheKey = `stats:link:${link._id}:city`; + const cacheKey = `stats:user-links:${user._id}:city`; if (DashboardService.CACHE_ENABLED) { stats = await cacheService.getObject(cacheKey); if (stats) { @@ -385,15 +381,18 @@ class DashboardService extends SiteService { } } - this.log.info('getLinkCityStats', 'generating link city stats report', { linkId: link._id }); + this.log.info('generating user city stats report', { userId: user._id }); const END_DATE = new Date(); const START_DATE = moment(END_DATE).subtract(7, 'day').toDate(); - stats = await LinkVisit.aggregate([ + const links = await Link.find({ user: user._id }).lean(); + const linkIds = links.map((link) => link._id); + + stats = await ResourceVisit.aggregate([ { $match: { - link: link._id, + resource: { $in: linkIds }, $and: [ { created: { $gt: START_DATE } }, { created: { $lt: END_DATE } }, diff --git a/app/services/link.js b/app/services/link.js index 45d45e6..505fb35 100644 --- a/app/services/link.js +++ b/app/services/link.js @@ -10,7 +10,6 @@ const mongoose = require('mongoose'); const pug = require('pug'); const Link = mongoose.model('Link'); -const LinkVisit = mongoose.model('LinkVisit'); const geoip = require('geoip-lite'); const striptags = require('striptags'); @@ -141,50 +140,10 @@ class LinkService extends SiteService { } async remove (link) { - this.log.debug('removing link visit records', { link: link._id }); - await LinkVisit.deleteMany({ link: link._id }); - const { resource: resourceService } = this.dtp.services; await resourceService.remove('Link', link._id); } - async recordVisit (link, req) { - const NOW = new Date(); - const visit = new LinkVisit(); - - visit.created = NOW; - visit.link = link._id; - - if (req.user) { - visit.user = req.user._id; - } - - /* - * We geo-analyze (but do not store) the IP address. - */ - const ipAddress = req.ip; - const geo = geoip.lookup(ipAddress); - if (geo) { - visit.geoip = { - country: geo.country, - region: geo.region, - eu: geo.eu, - timezone: geo.timezone, - city: geo.city, - }; - if (Array.isArray(geo.ll) && (geo.ll.length === 2)) { - visit.geoip.location = { - type: 'Point', - coordinates: geo.ll, - }; - } - } - - await visit.save(); - - await Link.updateOne({ _id: link._id }, { $inc: { 'stats.totalVisitCount': 1 } }); - } - async setItemOrder (links) { for await (const link of links) { this.log.debug('set link order', { link }); diff --git a/app/services/resource.js b/app/services/resource.js index 932999c..3dd3000 100644 --- a/app/services/resource.js +++ b/app/services/resource.js @@ -6,9 +6,12 @@ const { SiteService } = require('../../lib/site-lib'); +const geoip = require('geoip-lite'); + const mongoose = require('mongoose'); const ResourceView = mongoose.model('ResourceView'); +const ResourceVisit = mongoose.model('ResourceVisit'); class ResourceService extends SiteService { @@ -32,7 +35,11 @@ class ResourceService extends SiteService { * a view is being tracked. */ async recordView (req, resourceType, resourceId) { - const CURRENT_DAY = new Date(); + const Model = mongoose.model(resourceType); + const modelUpdate = { $inc: { } }; + + const NOW = new Date(); + const CURRENT_DAY = new Date(NOW); CURRENT_DAY.setHours(0, 0, 0, 0); let uniqueKey = req.ip.toString().trim().toLowerCase(); @@ -40,7 +47,6 @@ class ResourceService extends SiteService { uniqueKey += `:user:${req.user._id.toString()}`; } - const Model = mongoose.model(resourceType); const response = await ResourceView.updateOne( { created: CURRENT_DAY, @@ -55,19 +61,56 @@ class ResourceService extends SiteService { ); if (response.upsertedCount > 0) { - await Model.updateOne( - { _id: resourceId }, - { - $inc: { 'stats.uniqueVisitCount': 1 }, - }, - ); + modelUpdate.$inc['stats.uniqueViewCount'] = 1; } + + /* + * Record the ResourceVisit + */ + const visit = new ResourceVisit(); + + visit.created = NOW; + visit.resourceType = resourceType; + visit.resource = resourceId; + + if (req.user) { + visit.user = req.user._id; + } + + /* + * We geo-analyze (but do not store) the IP address. + */ + const ipAddress = req.ip; + const geo = geoip.lookup(ipAddress); + if (geo) { + visit.geoip = { + country: geo.country, + region: geo.region, + eu: geo.eu, + timezone: geo.timezone, + city: geo.city, + }; + if (Array.isArray(geo.ll) && (geo.ll.length === 2)) { + visit.geoip.location = { + type: 'Point', + coordinates: geo.ll, + }; + } + } + + await visit.save(); + + modelUpdate.$inc['stats.totalVisitCount'] = 1; + await Model.updateOne({ _id: resourceId }, modelUpdate); } async remove (resourceType, resource) { this.log.debug('removing resource view records', { resourceType, resourceId: resource._id }); await ResourceView.deleteMany({ resource: resource._id }); + this.log.debug('removing resource visit records', { resourceType, resourceId: resource._id }); + await ResourceVisit.deleteMany({ resource: resource._id }); + this.log.debug('removing resource', { resourceType, resourceId: resource._id }); const Model = mongoose.model(resourceType); await Model.deleteOne({ _id: resource._id }); diff --git a/app/views/dashboard/view.pug b/app/views/dashboard/view.pug index 11a0ea5..175f551 100644 --- a/app/views/dashboard/view.pug +++ b/app/views/dashboard/view.pug @@ -16,15 +16,27 @@ block content div(class="uk-width-1-1 uk-width-2-3@m") .uk-margin .uk-card.uk-card-secondary.uk-card-small.uk-card-body - h4.uk-card-title Link Visits #[small.uk-text-muted past 7 days] + h4.uk-card-title Profile Visits #[small.uk-text-muted past 7 days] canvas(id="profile-stats").visit-graph div(class="uk-visible@m", uk-grid).uk-flex-between.uk-text-small .uk-width-auto .uk-margin - div= moment(userVisitStats.start).format('MMM DD, YYYY h:mm:ss a') + div= moment(profileVisitStats.start).format('MMM DD, YYYY h:mm:ss a') + .uk-width-auto + .uk-margin + div= moment(profileVisitStats.end).format('MMM DD, YYYY h:mm:ss a') + + .uk-margin + .uk-card.uk-card-secondary.uk-card-small.uk-card-body + h4.uk-card-title Link Visits #[small.uk-text-muted past 7 days] + canvas(id="link-stats").visit-graph + div(class="uk-visible@m", uk-grid).uk-flex-between.uk-text-small + .uk-width-auto + .uk-margin + div= moment(linkVisitStats.start).format('MMM DD, YYYY h:mm:ss a') .uk-width-auto .uk-margin - div= moment(userVisitStats.end).format('MMM DD, YYYY h:mm:ss a') + div= moment(linkVisitStats.end).format('MMM DD, YYYY h:mm:ss a') .uk-margin .uk-card.uk-card-secondary.uk-card-small @@ -60,7 +72,7 @@ block content th Country th Visits tbody - each entry in userCountryStats.stats + each entry in linkCountryStats.stats tr td.uk-table-expand= entry.country ? entry.country : '--' td= entry.count @@ -76,7 +88,7 @@ block content th Country th Visits tbody - each entry in userCityStats.stats + each entry in linkCityStats.stats tr td= entry.city ? entry.city : '--' td(class="uk-visible@m")= entry.region ? entry.region : '--' @@ -88,5 +100,10 @@ block viewjs script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js") script. window.addEventListener('dtp-load', ( ) => { - dtp.app.renderProfileStats('canvas#profile-stats', !{JSON.stringify(userVisitStats.stats)}); + const data = { + profileVisitStats: !{JSON.stringify(profileVisitStats.stats)}, + linkVisitStats: !{JSON.stringify(linkVisitStats.stats)}, + }; + dtp.app.renderStatsGraph('canvas#profile-stats', 'Profile Visits', data.profileVisitStats); + dtp.app.renderStatsGraph('canvas#link-stats', 'Link Visits', data.linkVisitStats); }); \ No newline at end of file diff --git a/client/js/site-app.js b/client/js/site-app.js index 48ca4c5..bf8cf87 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -574,7 +574,7 @@ export default class DtpSiteApp extends DtpApp { } } - async renderProfileStats (selector, data) { + async renderStatsGraph (selector, title, data) { try { const canvas = document.querySelector(selector); const ctx = canvas.getContext('2d'); @@ -584,7 +584,7 @@ export default class DtpSiteApp extends DtpApp { labels: data.map((item) => new Date(item.date)), datasets: [ { - label: 'Link Visits', + label: title, data: data.map((item) => item.count), borderColor: CHART_LINE_USER, tension: 0.5, @@ -621,10 +621,7 @@ export default class DtpSiteApp extends DtpApp { plugins: { title: { display: false }, subtitle: { display: false }, - legend: { - display: true, - position: 'bottom', - }, + legend: { display: false }, }, maintainAspectRatio: true, aspectRatio: 16.0 / 9.0, @@ -639,12 +636,11 @@ export default class DtpSiteApp extends DtpApp { } else { chart.config.options.aspectRatio = 16.0 / 16.0; } - this.log.debug('renderProfileStats', 'chart resizing', { aspect: chart.config.options.aspectRatio }); }, }, }); } catch (error) { - this.log.error('renderProfileStats', 'failed to render profile stats', { error }); + this.log.error('renderStatsGraph', 'failed to render stats graph', { title, error }); UIkit.modal.alert(`Failed to render chart: ${error.message}`); } } diff --git a/data/patches/link-visit-port.js b/data/patches/link-visit-port.js new file mode 100644 index 0000000..dfd99c4 --- /dev/null +++ b/data/patches/link-visit-port.js @@ -0,0 +1,17 @@ +'use strict'; + +/* globals db */ + +const cursor = db.linkvisits.find({ }); +while (cursor.hasNext()) { + const linkVisit = cursor.next(); + print(`porting link visit: ${linkVisit._id.toString()}`); + db.resourcevisits.insertOne({ + _id: linkVisit._id, + created: linkVisit.created, + resourceType: 'Link', + resource: linkVisit.link, + user: linkVisit.user, + geoip: linkVisit.geoip, + }); +} \ No newline at end of file diff --git a/data/patches/user-stats.js b/data/patches/user-stats.js new file mode 100644 index 0000000..5db062b --- /dev/null +++ b/data/patches/user-stats.js @@ -0,0 +1,20 @@ +'use strict'; + +/* globals db */ + +const cursor = db.users.find({ }); +while (cursor.hasNext()) { + const user = cursor.next(); + print(`updating user: ${user.username}`); + db.users.updateOne( + { _id: user._id }, + { + $set: { + stats: { + uniqueVisitCount: 0, + totalVisitCount: 0, + }, + }, + }, + ); +} \ No newline at end of file