A web application allowing people to create an account, configure a profile, and share a list of URLs on that profile.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

443 lines
10 KiB

// dashboard.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Link = mongoose.model('Link');
const ResourceVisit = mongoose.model('ResourceVisit');
const moment = require('moment');
const { SiteService } = require('../../lib/site-lib');
class DashboardService extends SiteService {
static get CACHE_ENABLED ( ) { return process.env.LINKS_DASHBOARD_CACHE === 'enabled'; }
constructor (dtp) {
super(dtp, module.exports);
}
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:${resourceType.toLowerCase()}:${resourceId.toString()}:visit`;
if (DashboardService.CACHE_ENABLED) {
stats = await cacheService.getObject(cacheKey);
if (stats) {
return stats;
}
}
this.log.info('generating resource visit stats report', { resourceId });
const END_DATE = new Date();
const START_DATE = moment(END_DATE).subtract(7, 'day').toDate();
stats = await ResourceVisit.aggregate([
{
$match: {
resource: resourceId,
$and: [
{ created: { $gt: START_DATE } },
{ created: { $lt: END_DATE } },
],
},
},
{
$group: {
_id: {
year: { $year: '$created' },
month: { $month: '$created' },
day: { $dayOfMonth: '$created' },
hour: { $hour: '$created' },
},
count: { $sum: 1 },
},
},
{
$project: {
_id: false,
date: {
$dateFromParts: {
year: '$_id.year',
month: '$_id.month',
day: '$_id.day',
hour: '$_id.hour',
},
},
count: '$count',
},
},
{
$sort: { date: 1 },
},
]);
const response = { start: START_DATE, end: END_DATE, stats };
await cacheService.setObjectEx(cacheKey, 60 * 5, response);
return response;
}
/*
*
* RESOURCE COUNTRY STATS
*
*/
async getResourceCountryStats (resourceType, resourceId) {
const { cache: cacheService } = this.dtp.services;
let stats;
// 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) {
return stats;
}
}
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 ResourceVisit.aggregate([
{
$match: {
resource: resourceId,
$and: [
{ created: { $gt: START_DATE } },
{ created: { $lt: END_DATE } },
],
},
},
{
$group: {
_id: {
country: '$geoip.country',
},
count: { $sum: 1 },
},
},
{
$project: {
_id: false,
country: '$_id.country',
count: '$count',
},
},
{
$sort: { count: -1, country: 1 },
},
{
$limit: 10,
},
]);
const response = { start: START_DATE, end: END_DATE, stats };
await cacheService.setObjectEx(cacheKey, 60 * 5, response);
return response;
}
/*
*
* RESOURCE CITY STATS
*
*/
async getResourceCityStats (resourceType, resourceId) {
const { cache: cacheService } = this.dtp.services;
let stats;
// 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) {
return stats;
}
}
this.log.info('generating resource city stats report', { resourceId });
const END_DATE = new Date();
const START_DATE = moment(END_DATE).subtract(7, 'day').toDate();
stats = await ResourceVisit.aggregate([
{
$match: {
resource: resourceId,
$and: [
{ created: { $gt: START_DATE } },
{ created: { $lt: END_DATE } },
],
},
},
{
$group: {
_id: {
country: '$geoip.country',
region: '$geoip.region',
city: '$geoip.city',
},
count: { $sum: 1 },
},
},
{
$project: {
_id: false,
country: '$_id.country',
region: '$_id.region',
city: '$_id.city',
count: '$count',
},
},
{
$sort: { count: -1, city: 1, region: 1, country: 1 },
},
{
$limit: 10,
},
]);
const response = { start: START_DATE, end: END_DATE, stats };
await cacheService.setObjectEx(cacheKey, 60 * 5, response);
return response;
}
/*
*
* USER VISIT STATS
*
*/
async getUserVisitStats (user) {
const { cache: cacheService } = this.dtp.services;
let stats;
const cacheKey = `stats:user-links:${user._id}:visit`;
if (DashboardService.CACHE_ENABLED) {
stats = await cacheService.getObject(cacheKey);
if (stats) {
return stats;
}
}
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();
const links = await Link.find({ user: user._id }).lean();
const linkIds = links.map((link) => link._id);
stats = await ResourceVisit.aggregate([
{
$match: {
resource: { $in: linkIds },
$and: [
{ created: { $gt: START_DATE } },
{ created: { $lt: END_DATE } },
],
},
},
{
$group: {
_id: {
year: { $year: '$created' },
month: { $month: '$created' },
day: { $dayOfMonth: '$created' },
hour: { $hour: '$created' },
},
count: { $sum: 1 },
},
},
{
$project: {
_id: false,
date: {
$dateFromParts: {
year: '$_id.year',
month: '$_id.month',
day: '$_id.day',
hour: '$_id.hour',
},
},
count: '$count',
},
},
{
$sort: { date: 1 },
},
]);
const response = {
start: START_DATE,
end: END_DATE,
stats,
};
await cacheService.setObjectEx(cacheKey, 60 * 5, response);
return response;
}
/*
*
* USER COUNTRY STATS
*
*/
async getUserCountryStats (user) {
const { cache: cacheService } = this.dtp.services;
let stats;
const cacheKey = `stats:user-links:${user._id}:country`;
if (DashboardService.CACHE_ENABLED) {
stats = await cacheService.getObject(cacheKey);
if (stats) {
return stats;
}
}
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();
const links = await Link.find({ user: user._id }).lean();
const linkIds = links.map((link) => link._id);
stats = await ResourceVisit.aggregate([
{
$match: {
resource: { $in: linkIds },
$and: [
{ created: { $gt: START_DATE } },
{ created: { $lt: END_DATE } },
],
},
},
{
$group: {
_id: {
country: '$geoip.country',
},
count: { $sum: 1 },
},
},
{
$project: {
_id: false,
country: '$_id.country',
count: '$count',
},
},
{
$sort: { count: -1, country: 1 },
},
{
$limit: 10,
},
]);
const response = {
start: START_DATE,
end: END_DATE,
stats,
};
await cacheService.setObjectEx(cacheKey, 60 * 5, response);
return response;
}
/*
*
* USER CITY STATS
*
*/
async getUserCityStats (user) {
const { cache: cacheService } = this.dtp.services;
let stats;
const cacheKey = `stats:user-links:${user._id}:city`;
if (DashboardService.CACHE_ENABLED) {
stats = await cacheService.getObject(cacheKey);
if (stats) {
return stats;
}
}
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();
const links = await Link.find({ user: user._id }).lean();
const linkIds = links.map((link) => link._id);
stats = await ResourceVisit.aggregate([
{
$match: {
resource: { $in: linkIds },
$and: [
{ created: { $gt: START_DATE } },
{ created: { $lt: END_DATE } },
],
},
},
{
$group: {
_id: {
country: '$geoip.country',
region: '$geoip.region',
city: '$geoip.city',
},
count: { $sum: 1 },
},
},
{
$project: {
_id: false,
country: '$_id.country',
region: '$_id.region',
city: '$_id.city',
count: '$count',
},
},
{
$sort: { count: -1, city: 1, region: 1, country: 1 },
},
{
$limit: 10,
},
]);
const response = {
start: START_DATE,
end: END_DATE,
stats,
};
await cacheService.setObjectEx(cacheKey, 60 * 5, response);
return response;
}
}
module.exports = {
slug: 'dashboard',
name: 'dashboard',
create: (dtp) => { return new DashboardService(dtp); },
};