Browse Source

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
master
Rob Colbert 2 years ago
parent
commit
c1449a16ac
  1. 7
      app/controllers/dashboard.js
  2. 5
      app/controllers/home.js
  3. 12
      app/controllers/link.js
  4. 9
      app/models/lib/geo-types.js
  5. 35
      app/models/link-visit.js
  6. 29
      app/models/resource-visit.js
  7. 7
      app/models/user.js
  8. 173
      app/services/dashboard.js
  9. 41
      app/services/link.js
  10. 59
      app/services/resource.js
  11. 29
      app/views/dashboard/view.pug
  12. 12
      client/js/site-app.js
  13. 17
      data/patches/link-visit-port.js
  14. 20
      data/patches/user-stats.js

7
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) {

5
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 });

12
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) {

9
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 },
});

35
app/models/link-visit.js

@ -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);

29
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);

7
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);

173
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 } },

41
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 });

59
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 });

29
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);
});

12
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}`);
}
}

17
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,
});
}

20
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,
},
},
},
);
}
Loading…
Cancel
Save