Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a

## [Unreleased]

### Added

- Page view duration limit and interval is configurable now at system level, could be changed via API and Settings panel (thanks @marek629, #381)

### Changed

- The official Docker image is now based on Node.js 22 (#343)
Expand Down
2 changes: 1 addition & 1 deletion dist/index.css

Large diffs are not rendered by default.

218 changes: 124 additions & 94 deletions dist/index.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"devDependencies": {
"@apollo/client": "^3.7.2",
"@electerious/eslint-config": "^3.5.0",
"@types/sinon": "^17.0.3",
"ava": "5.1.0",
"classnames": "^2.3.1",
"coveralls": "^3.1.1",
Expand All @@ -73,6 +74,7 @@
"normalize.css": "^8.0.1",
"nyc": "^15.1.0",
"prop-types": "^15.8.1",
"ramda": "^0.30.1",
"react": "^18.1.0",
"react-apollo-network-status": "^5.0.1",
"react-dom": "^18.1.0",
Expand All @@ -83,6 +85,7 @@
"rosid-handler-sass": "^8.0.0",
"s-ago": "^2.2.0",
"shortid": "^2.2.16",
"sinon": "^19.0.2",
"test-listen": "^1.1.0",
"url-pattern": "^1.0.3"
},
Expand Down
6 changes: 3 additions & 3 deletions src/aggregations/aggregateActiveVisitors.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { DURATIONS_LIMIT, DURATIONS_INTERVAL } = require('../constants/durations')
const constants = require('../constants/durations')
const matchDomains = require('../stages/matchDomains')

module.exports = (ids, dateDetails) => {
Expand All @@ -19,10 +19,10 @@ module.exports = (ids, dateDetails) => {
}

// Ignore users that are on the page for too long
aggregation[0].$match.created = { $gte: dateDetails.lastMilliseconds(DURATIONS_LIMIT) }
aggregation[0].$match.created = { $gte: dateDetails.lastMilliseconds(constants.DURATIONS_LIMIT) }

// Ignore users that aren't active anymore
aggregation[0].$match.updated = { $gte: dateDetails.lastMilliseconds(DURATIONS_INTERVAL * 2) }
aggregation[0].$match.updated = { $gte: dateDetails.lastMilliseconds(constants.DURATIONS_INTERVAL * 2) }

return aggregation
}
12 changes: 8 additions & 4 deletions src/constants/durations.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
'use strict'

const { hour } = require('../utils/times')
const { customise } = require('../utils/constants')
const { hour, second } = require('../utils/times')

const DURATIONS_INTERVAL = 15000
const DURATIONS_INTERVAL = 15 * second
const DURATIONS_LIMIT = hour / 2

module.exports = {
module.exports = customise({
DURATIONS_INTERVAL,
DURATIONS_LIMIT,
}
}, [
'DURATIONS_INTERVAL',
'DURATIONS_LIMIT',
])
40 changes: 40 additions & 0 deletions src/database/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

const Constant = require('../models/Constant')

const response = (entry) => ({
id: entry.id,
name: entry.name,
value: entry.value,
unit: entry.unit,
created: entry.created,
updated: entry.updated,
})

const enhance = (entry) => {
return entry == null ? entry : response(entry)
}

const get = async (name) => enhance(
await Constant.findOne({ name }),
)

const update = async (name, value, unit) => enhance(
await Constant.findOneAndUpdate({
name,
}, {
$set: {
updated: Date.now(),
value,
unit,
},
}, {
new: true,
upsert: true,
}),
)

module.exports = {
get,
update,
}
38 changes: 38 additions & 0 deletions src/models/Constant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict'

const mongoose = require('mongoose')
const uuid = require('crypto').randomUUID

const schema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true,
default: () => uuid(),
},
created: {
type: Date,
required: true,
default: Date.now,
},
updated: {
type: Date,
required: true,
default: Date.now,
},
name: {
type: String,
required: true,
unique: true,
},
value: {
type: Number,
required: true,
},
unit: {
type: String,
required: true,
},
})

module.exports = mongoose.model('Constant', schema)
53 changes: 53 additions & 0 deletions src/resolvers/contants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const { DURATIONS_INTERVAL, DURATIONS_LIMIT } = require('../constants/durations')
const constants = require('../database/constants')
const requireAuth = require('../middlewares/requireAuth')
const pipe = require('../utils/pipe')

const response = (entry) => ({
created: entry.created,
updated: entry.updated,
id: entry.name,
value: entry.value,
unit: entry.unit,
})

const toApi = ({ name, ...data }) => ({
...data,
id: name,
})
const interval = async () => {
const name = 'DURATIONS_INTERVAL'
return toApi(await constants.get(name) || {
name,
value: DURATIONS_INTERVAL,
unit: 'seconds',
})
}
const limit = async () => {
const name = 'DURATIONS_LIMIT'
return toApi(await constants.get(name) || {
name,
value: DURATIONS_LIMIT,
unit: 'minutes',
})
}

module.exports = {
Query: {
constants: pipe(requireAuth, async () => ({
durationsInterval: await interval(),
durationsLimit: await limit(),
})),
},
Mutation: {
setConstant: pipe(requireAuth, async (parent, { input }) => {
const { id, value, unit } = input

const entry = await constants.update(id, value, unit)
return {
success: true,
payload: response(entry),
}
}),
},
}
1 change: 1 addition & 0 deletions src/resolvers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { mergeResolvers } = require('@graphql-tools/merge')
module.exports = mergeResolvers([
require('./tokens'),
require('./permanentTokens'),
require('./contants'),
require('./records'),
require('./domains'),
require('./events'),
Expand Down
5 changes: 3 additions & 2 deletions src/stages/matchLimit.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use strict'

const { DURATIONS_LIMIT } = require('../constants/durations')
const constants = require('../constants/durations')

module.exports = () => {
// Some visitors keep sites open in the background. Their duration is often
// way above the limit. This distorts the average and should be omitted.
return {
$match: {
duration: {
$lt: DURATIONS_LIMIT,
// get the latest value using Proxy object
$lt: constants.DURATIONS_LIMIT,
},
},
}
Expand Down
3 changes: 2 additions & 1 deletion src/stages/projectMinInterval.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use strict'

const { DURATIONS_INTERVAL } = require('../constants/durations')
const constants = require('../constants/durations')

module.exports = () => {
// Visits below the tracking interval will have a duration of zero. That's
// incorrect as visitors spent time on the site, but just not enough. This
// step sets the minimum duration to the half of the tracking interval.
// This value is a compromise that doesn't influence the average too much.
const { DURATIONS_INTERVAL } = constants
return {
$project: {
created: '$created',
Expand Down
98 changes: 98 additions & 0 deletions src/types/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'

const { gql } = require('apollo-server-micro')

module.exports = gql`
"""
Unique name of the constant.
"""
enum ConstantName {
"""
Time granularity in page views duration analitics.
"""
DURATIONS_INTERVAL
"""
Maximum duration of page view.
"""
DURATIONS_LIMIT
}

"""
Measure unit of the constant, usefull at UI context.
"""
enum ConstantUnit {
hours
minutes
seconds
}

"""
Constants data.
"""
type Constants {
id: ConstantName!
"""
Identifies the date and time when the object was created.
"""
created: DateTime
"""
Identifies the date and time when the object was updated.
"""
updated: DateTime
"""
Value of the constant.
"""
value: UnsignedInt!
unit: ConstantUnit!
}

"""
System constants which could be customized by user
"""
type SystemConstants {
"""
Durations limit of page view, in milliseconds.
Visits longer than the limit will be ignored.
"""
durationsLimit: Constants
"""
Durations interval of page view, in milliseconds.
It's a kind of time resolution of visits.
"""
durationsInterval: Constants
}

type Query {
"""
System constants which could be customized by user
"""
constants: SystemConstants!
}

input ConstantsInput {
id: ConstantName!
"""
Value of the constant.
"""
value: UnsignedInt!
unit: ConstantUnit!
}

type ConstantsPayload {
"""
Indicates if the operation finished successfully
"""
success: Boolean!
"""
Post-operation payload
"""
payload: Constants!
}

type Mutation {
"""
Set new value of given constant.
"""
setConstant(input: ConstantsInput!): ConstantsPayload!
}
`
1 change: 1 addition & 0 deletions src/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { mergeTypeDefs } = require('@graphql-tools/merge')
module.exports = mergeTypeDefs([
require('./tokens'),
require('./permanentTokens'),
require('./constants'),
require('./records'),
require('./domains'),
require('./events'),
Expand Down
9 changes: 9 additions & 0 deletions src/ui/scripts/api/fragments/constantFields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { gql } from '@apollo/client'

export default gql`
fragment constantFields on Constants {
id
value
unit
}
`
Loading