mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
commit
681852b225
10
README.md
10
README.md
@ -82,6 +82,16 @@ To build the umami container and start up a Postgres database, run:
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Getting updates
|
||||
|
||||
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
|
||||
|
||||
```
|
||||
git pull
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
@ -1,19 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setDateRange } from 'redux/actions/websites';
|
||||
import Button from './Button';
|
||||
import Refresh from 'assets/redo.svg';
|
||||
import Dots from 'assets/ellipsis-h.svg';
|
||||
import { useDateRange } from 'hooks/useDateRange';
|
||||
import { getDateRange } from '../../lib/date';
|
||||
|
||||
export default function RefreshButton({ websiteId }) {
|
||||
const dispatch = useDispatch();
|
||||
const dateRange = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]);
|
||||
|
||||
function handleClick() {
|
||||
if (dateRange) {
|
||||
dispatch(setDateRange(websiteId, dateRange));
|
||||
setLoading(true);
|
||||
dispatch(setDateRange(websiteId, getDateRange(dateRange.value)));
|
||||
}
|
||||
}
|
||||
|
||||
return <Button icon={<Refresh />} size="small" onClick={handleClick} />;
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [completed]);
|
||||
|
||||
return <Button icon={loading ? <Dots /> : <Refresh />} size="small" onClick={handleClick} />;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||
import ChartJS from 'chart.js';
|
||||
import styles from './BarChart.module.css';
|
||||
import { format } from 'date-fns';
|
||||
import { formatLongNumber } from '../../lib/format';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
|
||||
export default function BarChart({
|
||||
chartId,
|
||||
@ -22,30 +22,39 @@ export default function BarChart({
|
||||
const chart = useRef();
|
||||
const [tooltip, setTooltip] = useState({});
|
||||
|
||||
const renderXLabel = (label, index, values) => {
|
||||
function renderXLabel(label, index, values) {
|
||||
const d = new Date(values[index].value);
|
||||
const n = records;
|
||||
const w = canvas.current.width;
|
||||
|
||||
switch (unit) {
|
||||
case 'hour':
|
||||
return format(d, 'ha');
|
||||
case 'day':
|
||||
if (n >= 15) {
|
||||
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
|
||||
if (records > 31) {
|
||||
if (w <= 500) {
|
||||
return index % 10 === 0 ? format(d, 'M/d') : '';
|
||||
}
|
||||
return index % 5 === 0 ? format(d, 'M/d') : '';
|
||||
}
|
||||
if (w <= 500) {
|
||||
return index % 2 === 0 ? format(d, 'MMM d') : '';
|
||||
}
|
||||
return format(d, 'EEE M/d');
|
||||
case 'month':
|
||||
if (w <= 660) {
|
||||
return format(d, 'MMM');
|
||||
}
|
||||
return format(d, 'MMMM');
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const renderYLabel = label => {
|
||||
function renderYLabel(label) {
|
||||
return +label > 1 ? formatLongNumber(label) : label;
|
||||
};
|
||||
}
|
||||
|
||||
const renderTooltip = model => {
|
||||
function renderTooltip(model) {
|
||||
const { opacity, title, body, labelColors } = model;
|
||||
|
||||
if (!opacity) {
|
||||
@ -60,9 +69,9 @@ export default function BarChart({
|
||||
labelColor: labelColors[0].backgroundColor,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const createChart = () => {
|
||||
function createChart() {
|
||||
const options = {
|
||||
animation: {
|
||||
duration: animationDuration,
|
||||
@ -119,9 +128,9 @@ export default function BarChart({
|
||||
},
|
||||
options,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
function updateChart() {
|
||||
const { options } = chart.current;
|
||||
|
||||
options.scales.xAxes[0].time.unit = unit;
|
||||
@ -129,7 +138,7 @@ export default function BarChart({
|
||||
options.animation.duration = animationDuration;
|
||||
|
||||
onUpdate(chart.current);
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (datasets) {
|
||||
|
@ -21,7 +21,7 @@ export default function WebsiteHeader({ websiteId, title, showLink = false }) {
|
||||
<Button
|
||||
icon={<Arrow />}
|
||||
onClick={() =>
|
||||
router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
|
||||
router.push('/website/[...id]', `/website/${websiteId}/${title}`, {
|
||||
shallow: true,
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get } from 'lib/web';
|
||||
import { updateQuery } from 'redux/actions/queries';
|
||||
|
||||
export default function useFetch(url, params = {}, options = {}) {
|
||||
const dispatch = useDispatch();
|
||||
const [data, setData] = useState();
|
||||
const [error, setError] = useState();
|
||||
const keys = Object.keys(params)
|
||||
@ -12,7 +15,11 @@ export default function useFetch(url, params = {}, options = {}) {
|
||||
async function loadData() {
|
||||
try {
|
||||
setError(null);
|
||||
const time = performance.now();
|
||||
const data = await get(url, params);
|
||||
|
||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||
|
||||
setData(data);
|
||||
onDataLoad(data);
|
||||
} catch (e) {
|
||||
|
@ -34,7 +34,6 @@ export default prisma;
|
||||
|
||||
export async function runQuery(query) {
|
||||
return query.catch(e => {
|
||||
console.error(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
@ -13,7 +13,11 @@ export const urlFilter = (data, { raw }) => {
|
||||
|
||||
const cleanUrl = url => {
|
||||
try {
|
||||
const { pathname, searchParams } = new URL(url);
|
||||
const { pathname, search, searchParams } = new URL(url);
|
||||
|
||||
if (search.startsWith('?/')) {
|
||||
return `${pathname}${search}`;
|
||||
}
|
||||
|
||||
const path = removeTrailingSlash(pathname);
|
||||
const ref = searchParams.get('ref');
|
||||
|
116
lib/queries.js
116
lib/queries.js
@ -252,8 +252,9 @@ export function getMetrics(website_id, start_at, end_at) {
|
||||
const db = getDatabase();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select sum(t.c) as "pageviews",
|
||||
count(distinct t.session_id) as "uniques",
|
||||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||
@ -269,15 +270,17 @@ export function getMetrics(website_id, start_at, end_at) {
|
||||
group by 1, 2
|
||||
) t
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select sum(t.c) as "pageviews",
|
||||
count(distinct t.session_id) as "uniques",
|
||||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||
@ -293,9 +296,10 @@ export function getMetrics(website_id, start_at, end_at) {
|
||||
group by 1, 2
|
||||
) t
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -313,8 +317,9 @@ export function getPageviews(
|
||||
const db = getDatabase();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select ${getDateQuery(db, 'created_at', unit, timezone)} t,
|
||||
count(${count}) y
|
||||
from pageview
|
||||
@ -323,15 +328,17 @@ export function getPageviews(
|
||||
group by 1
|
||||
order by 1
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select ${getDateQuery(db, 'created_at', unit, timezone)} t,
|
||||
count(${count}) y
|
||||
from pageview
|
||||
@ -340,9 +347,10 @@ export function getPageviews(
|
||||
group by 1
|
||||
order by 1
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -355,8 +363,9 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
|
||||
const filter = domain ? `and ${type} not like '%${domain}%'` : '';
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select distinct ${type} x, count(*) y
|
||||
from ${table}
|
||||
where website_id=$1
|
||||
@ -365,15 +374,17 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
|
||||
group by 1
|
||||
order by 2 desc
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select distinct ${type} x, count(*) y
|
||||
from ${table}
|
||||
where website_id=?
|
||||
@ -382,9 +393,10 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
|
||||
group by 1
|
||||
order by 2 desc
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -396,28 +408,32 @@ export function getActiveVisitors(website_id) {
|
||||
const date = subMinutes(new Date(), 5);
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select count(distinct session_id) x
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at >= $2
|
||||
`,
|
||||
website_id,
|
||||
date,
|
||||
website_id,
|
||||
date,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select count(distinct session_id) x
|
||||
from pageview
|
||||
where website_id=?
|
||||
and created_at >= ?
|
||||
`,
|
||||
website_id,
|
||||
date,
|
||||
website_id,
|
||||
date,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -428,8 +444,9 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
|
||||
const db = getDatabase();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select
|
||||
event_value x,
|
||||
${getDateQuery(db, 'created_at', unit, timezone)} t,
|
||||
@ -440,15 +457,17 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
|
||||
group by 1, 2
|
||||
order by 2
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return prisma.$queryRaw(
|
||||
`
|
||||
return runQuery(
|
||||
prisma.$queryRaw(
|
||||
`
|
||||
select
|
||||
event_value x,
|
||||
${getDateQuery(db, 'created_at', unit, timezone)} t,
|
||||
@ -459,9 +478,10 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
|
||||
group by 1, 2
|
||||
order by 2
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
20
package.json
20
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "0.20.0",
|
||||
"version": "0.21.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
@ -39,14 +39,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "2.5.1",
|
||||
"@prisma/client": "2.6.1",
|
||||
"@reduxjs/toolkit": "^1.4.0",
|
||||
"bcrypt": "^5.0.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"classnames": "^2.2.6",
|
||||
"cookie": "^0.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.16.0",
|
||||
"date-fns": "^2.16.1",
|
||||
"date-fns-tz": "^1.0.10",
|
||||
"detect-browser": "^5.1.1",
|
||||
"dotenv": "^8.2.0",
|
||||
@ -57,7 +57,7 @@
|
||||
"jose": "^1.28.0",
|
||||
"maxmind": "^4.1.4",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"next": "^9.5.2",
|
||||
"next": "^9.5.3",
|
||||
"promise-polyfill": "^8.1.3",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
@ -75,30 +75,30 @@
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/cli": "2.5.1",
|
||||
"@prisma/cli": "2.6.1",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-replace": "^2.3.3",
|
||||
"@svgr/webpack": "^5.4.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv-cli": "^3.2.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint": "^7.8.1",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.20.6",
|
||||
"eslint-plugin-react-hooks": "^4.1.0",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.13",
|
||||
"lint-staged": "^10.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-flexbugs-fixes": "^4.2.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.1.1",
|
||||
"prettier-eslint": "^11.0.0",
|
||||
"rollup": "^2.26.6",
|
||||
"rollup": "^2.26.9",
|
||||
"rollup-plugin-hashbang": "^2.2.2",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"stylelint": "^13.6.0",
|
||||
"rollup-plugin-terser": "^7.0.1",
|
||||
"stylelint": "^13.7.0",
|
||||
"stylelint-config-css-modules": "^2.2.0",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
"stylelint-config-recommended": "^3.0.0"
|
||||
|
17
redux/actions/queries.js
Normal file
17
redux/actions/queries.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const queries = createSlice({
|
||||
name: 'queries',
|
||||
initialState: {},
|
||||
reducers: {
|
||||
updateQuery(state, action) {
|
||||
const { url, ...data } = action.payload;
|
||||
state[url] = data;
|
||||
return state;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateQuery } = queries.actions;
|
||||
|
||||
export default queries.reducer;
|
@ -1,33 +1,29 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import produce from 'immer';
|
||||
|
||||
const websites = createSlice({
|
||||
name: 'user',
|
||||
name: 'websites',
|
||||
initialState: {},
|
||||
reducers: {
|
||||
updateWebsites(state, action) {
|
||||
state = action.payload;
|
||||
return state;
|
||||
},
|
||||
updateWebsite(state, action) {
|
||||
const { websiteId, ...data } = action.payload;
|
||||
state[websiteId] = data;
|
||||
return state;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateWebsites } = websites.actions;
|
||||
export const { updateWebsites, updateWebsite } = websites.actions;
|
||||
|
||||
export default websites.reducer;
|
||||
|
||||
export function setDateRange(websiteId, dateRange) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
let { websites = {} } = state;
|
||||
|
||||
websites = produce(websites, draft => {
|
||||
if (!draft[websiteId]) {
|
||||
draft[websiteId] = {};
|
||||
}
|
||||
draft[websiteId].dateRange = { ...dateRange, modified: Date.now() };
|
||||
});
|
||||
|
||||
return dispatch(updateWebsites(websites));
|
||||
return dispatch => {
|
||||
return dispatch(
|
||||
updateWebsite({ websiteId, dateRange: { ...dateRange, modified: Date.now() } }),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import user from './actions/user';
|
||||
import websites from './actions/websites';
|
||||
import queries from './actions/queries';
|
||||
|
||||
export default combineReducers({ user, websites });
|
||||
export default combineReducers({ user, websites, queries });
|
||||
|
@ -61,12 +61,13 @@ import { removeTrailingSlash } from '../lib/url';
|
||||
const handlePush = (state, title, navigatedURL) => {
|
||||
removeEvents();
|
||||
currentRef = currentUrl;
|
||||
const newUrl = navigatedURL.toString();
|
||||
|
||||
if (navigatedURL.toString().startsWith('http')) {
|
||||
const url = new URL(navigatedURL.toString());
|
||||
if (newUrl.startsWith('http')) {
|
||||
const url = new URL(newUrl);
|
||||
currentUrl = `${url.pathname}${url.search}`;
|
||||
} else {
|
||||
currentUrl = navigatedURL.toString();
|
||||
currentUrl = newUrl;
|
||||
}
|
||||
|
||||
pageView();
|
||||
@ -101,4 +102,8 @@ import { removeTrailingSlash } from '../lib/url';
|
||||
/* Start */
|
||||
|
||||
pageView();
|
||||
|
||||
if (!window.umami) {
|
||||
window.umami = event_value => collect('event', { event_type: 'custom', event_value });
|
||||
}
|
||||
})(window);
|
||||
|
Loading…
Reference in New Issue
Block a user