Browse Source

Initial commit.

do-not-track
Mike Cao 1 year ago
commit
f7f0c05e12
27 changed files with 13028 additions and 0 deletions
  1. +21
    -0
      .eslintrc.json
  2. +34
    -0
      .gitignore
  3. +7
    -0
      .prettierrc.json
  4. +17
    -0
      .stylelintrc.json
  5. +1
    -0
      README.md
  6. +5
    -0
      components/footer.js
  7. +7
    -0
      components/header.js
  8. +23
    -0
      components/layout.js
  9. +5
    -0
      jsconfig.json
  10. +65
    -0
      lib/db.js
  11. +72
    -0
      lib/utils.js
  12. +15
    -0
      next.config.js
  13. +67
    -0
      package.json
  14. +10
    -0
      pages/404.js
  15. +7
    -0
      pages/_app.js
  16. +16
    -0
      pages/api/collect.js
  17. +28
    -0
      pages/api/session.js
  18. +10
    -0
      pages/index.js
  19. +17
    -0
      postcss.config.js
  20. +48
    -0
      prisma/schema.prisma
  21. +1
    -0
      public/umami.js
  22. +24
    -0
      rollup.config.js
  23. +39
    -0
      scripts/umami/index.js
  24. +33
    -0
      sql/schema.sql
  25. +3981
    -0
      styles/bootstrap-grid.css
  26. +25
    -0
      styles/index.css
  27. +8450
    -0
      yarn.lock

+ 21
- 0
.eslintrc.json View File

@ -0,0 +1,21 @@
{
"env": {
"browser": true,
"es2020": true
},
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "prettier/react"],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["react"],
"rules": {
"react/react-in-jsx-scope": "off"
},
"globals": {
"React": "writable"
}
}

+ 34
- 0
.gitignore View File

@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
.idea
*.iml
.env
.env*.local
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.development.local
.env.test.local
.env.production.local

+ 7
- 0
.prettierrc.json View File

@ -0,0 +1,7 @@
{
"arrowParens": "avoid",
"endOfLine": "lf",
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all"
}

+ 17
- 0
.stylelintrc.json View File

@ -0,0 +1,17 @@
{
"extends": [
"stylelint-config-recommended",
"stylelint-config-css-modules",
"stylelint-config-prettier"
],
"rules": {
"no-descending-specificity": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "horizontal", "vertical"]
}
]
},
"ignoreFiles": ["**/*.js"]
}

+ 1
- 0
README.md View File

@ -0,0 +1 @@
umami - deliciously simple web stats

+ 5
- 0
components/footer.js View File

@ -0,0 +1,5 @@
import React from 'react';
export default function Footer() {
return <footer className="container">Footer</footer>;
}

+ 7
- 0
components/header.js View File

@ -0,0 +1,7 @@
import React from 'react';
export default function Header() {
return <header className="container">
<h1>umami</h1>
</header>;
}

+ 23
- 0
components/layout.js View File

@ -0,0 +1,23 @@
import React from 'react';
import Head from 'next/head';
import Header from 'components/header';
import Footer from 'components/footer';
export default function Layout({ title, children }) {
return (
<>
<Head>
<title>umami{title && ` - ${title}`}</title>
<link rel="icon" href="/favicon.ico" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400&display=swap"
rel="stylesheet"
/>
<script async defer data-website-id="865234ad-6a92-11e7-8846-b05adad3f099" src="/umami.js" />
</Head>
<Header />
<main className="container">{children}</main>
<Footer />
</>
);
}

+ 5
- 0
jsconfig.json View File

@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "."
}
}

+ 65
- 0
lib/db.js View File

@ -0,0 +1,65 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function runQuery(query) {
return query
.catch(e => {
throw e;
})
.finally(async () => {
await prisma.disconnect();
});
}
export async function getWebsite(website_id) {
return runQuery(
prisma.website.findOne({
where: {
website_id,
},
}),
);
}
export async function createSession(website_id, session_id, data) {
await runQuery(
prisma.session.create({
data: {
session_id,
website: {
connect: {
website_id,
},
},
...data,
},
}),
);
}
export async function getSession(session_id) {
return runQuery(
prisma.session.findOne({
where: {
session_id,
},
}),
);
}
export async function savePageView(session_id, url, referrer) {
return runQuery(
prisma.pageview.create({
data: {
session: {
connect: {
session_id,
},
},
url,
referrer,
},
}),
);
}

+ 72
- 0
lib/utils.js View File

@ -0,0 +1,72 @@
import crypto from 'crypto';
import { v5 as uuid } from 'uuid';
import requestIp from 'request-ip';
import { browserName, detectOS } from 'detect-browser';
export function md5(s) {
return crypto.createHash('md5').update(s).digest('hex');
}
export function hash(s) {
return uuid(s, md5(process.env.HASH_SALT));
}
export function getIpAddress(req) {
if (req.headers['cf-connecting-ip']) {
return req.headers['cf-connecting-ip'];
}
return requestIp.getClientIp(req);
}
export function getDevice(req) {
const userAgent = req.headers['user-agent'];
const browser = browserName(userAgent);
const os = detectOS(userAgent);
return { userAgent, browser, os };
}
export function getCountry(req) {
return req.headers['cf-ipcountry'];
}
export function parseSessionRequest(req) {
const ip = getIpAddress(req);
const { website_id, screen, language } = JSON.parse(req.body);
const { userAgent, browser, os } = getDevice(req);
const country = getCountry(req);
const session_id = hash(`${website_id}${ip}${userAgent}${os}`);
return {
website_id,
session_id,
browser,
os,
screen,
language,
country,
};
}
export function parseCollectRequest(req) {
const { type, payload } = JSON.parse(req.body);
if (payload.session) {
const {
url,
referrer,
session: { website_id, session_id, time, hash: validationHash },
} = payload;
if (hash(`${website_id}${session_id}${time}`) === validationHash) {
return {
type,
session_id,
url,
referrer,
};
}
}
return null;
}

+ 15
- 0
next.config.js View File

@ -0,0 +1,15 @@
require('dotenv').config();
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
issuer: {
test: /\.js$/,
},
use: ['@svgr/webpack'],
});
return config;
},
};

+ 67
- 0
package.json View File

@ -0,0 +1,67 @@
{
"name": "umami",
"version": "0.1.0",
"description": "Deliciously simple website analytics",
"main": "index.js",
"author": "Mike Cao",
"license": "MIT",
"scripts": {
"dev": "next dev -p 8000",
"build": "next build",
"start": "next start",
"build-script": "rollup -c"
},
"lint-staged": {
"**/*.js": [
"prettier --write"
],
"**/*.css": [
"stylelint --fix",
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"dependencies": {
"@prisma/client": "2.2.2",
"classnames": "^2.2.6",
"date-fns": "^2.14.0",
"detect-browser": "^5.1.1",
"dotenv": "^8.2.0",
"next": "9.3.5",
"node-fetch": "^2.6.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"request-ip": "^2.1.3",
"uuid": "^8.2.0",
"whatwg-fetch": "^3.2.0"
},
"devDependencies": {
"@prisma/cli": "2.2.2",
"@rollup/plugin-node-resolve": "^8.4.0",
"@svgr/webpack": "^5.4.0",
"eslint": "^7.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.4",
"husky": "^4.2.5",
"less": "^3.11.3",
"lint-staged": "^10.2.9",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.0.5",
"prettier-eslint": "^10.1.1",
"rollup": "^2.21.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^6.1.0",
"stylelint": "^13.6.0",
"stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0"
}
}

+ 10
- 0
pages/404.js View File

@ -0,0 +1,10 @@
import React from 'react';
import Layout from 'components/layout';
export default function Custom404() {
return (
<Layout title="404 - Page Not Found">
<h1>oops</h1>
</Layout>
);
}

+ 7
- 0
pages/_app.js View File

@ -0,0 +1,7 @@
import React from 'react';
import 'styles/index.css';
import 'styles/bootstrap-grid.css';
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}

+ 16
- 0
pages/api/collect.js View File

@ -0,0 +1,16 @@
import { parseCollectRequest } from 'lib/utils';
import { savePageView } from '../../lib/db';
export default async (req, res) => {
const values = parseCollectRequest(req);
if (values) {
const { type, session_id, url, referrer } = values;
if (type === 'pageview') {
await savePageView(session_id, url, referrer);
}
}
res.status(200).json({ status: 'ok' });
};

+ 28
- 0
pages/api/session.js View File

@ -0,0 +1,28 @@
import { getWebsite, getSession, createSession } from 'lib/db';
import { hash, parseSessionRequest } from 'lib/utils';
export default async (req, res) => {
let result = { time: Date.now() };
const { website_id, session_id, browser, os, screen, language, country } = parseSessionRequest(
req,
);
const website = await getWebsite(website_id);
if (website) {
const session = await getSession(session_id);
if (!session) {
await createSession(website_id, session_id, { browser, os, screen, language, country });
}
result = {
...result,
session_id,
website_id,
hash: hash(`${website_id}${session_id}${result.time}`),
};
}
res.status(200).json(result);
};

+ 10
- 0
pages/index.js View File

@ -0,0 +1,10 @@
import React from 'react';
import Layout from 'components/layout';
export default function Home() {
return (
<Layout>
Hello.
</Layout>
);
}

+ 17
- 0
postcss.config.js View File

@ -0,0 +1,17 @@
module.exports = {
plugins: [
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'custom-properties': false,
},
},
],
],
};

+ 48
- 0
prisma/schema.prisma View File

@ -0,0 +1,48 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model event {
created_at DateTime? @default(now())
event_id Int @default(autoincrement()) @id
event_type String
event_value String
session_id String?
url String
session session? @relation(fields: [session_id], references: [session_id])
}
model pageview {
created_at DateTime? @default(now())
referrer String?
session_id String?
url String
view_id Int @default(autoincrement()) @id
session session? @relation(fields: [session_id], references: [session_id])
}
model session {
browser String?
country String?
created_at DateTime? @default(now())
language String?
os String?
screen String?
session_id String @id
website_id String?
website website? @relation(fields: [website_id], references: [website_id])
event event[]
pageview pageview[]
}
model website {
created_at DateTime? @default(now())
hostname String @unique
website_id String @id
session session[]
}

+ 1
- 0
public/umami.js
File diff suppressed because it is too large
View File


+ 24
- 0
rollup.config.js View File

@ -0,0 +1,24 @@
import 'dotenv/config';
import { terser } from 'rollup-plugin-terser';
import replace from 'rollup-plugin-replace';
import { nodeResolve } from '@rollup/plugin-node-resolve';
export default {
input: 'scripts/umami/index.js',
output: {
file: 'public/umami.js',
format: 'iife',
globals: {
'detect-browser': 'detectBrowser',
'whatwg-fetch': 'fetch',
},
plugins: [terser()],
},
context: 'window',
plugins: [
nodeResolve(),
replace({
'process.env.UMAMI_URL': JSON.stringify(process.env.UMAMI_URL),
}),
],
};

+ 39
- 0
scripts/umami/index.js View File

@ -0,0 +1,39 @@
import 'whatwg-fetch';
function post(url, params) {
return fetch(url, {
method: 'post',
body: JSON.stringify(params),
}).then(res => res.json());
}
(async () => {
const script = document.querySelector('script[data-website-id]');
const website_id = script.getAttribute('data-website-id');
if (website_id) {
const { width, height } = window.screen;
const { language } = window.navigator;
const { hostname, pathname, search } = window.location;
const referrer = window.document.referrer;
const screen = `${width}x${height}`;
const url = `${pathname}${search}`;
if (!window.localStorage.getItem('umami.session')) {
const session = await post(`${process.env.UMAMI_URL}/api/session`, {
website_id,
hostname,
url,
screen,
language,
});
console.log(session);
window.localStorage.setItem('umami.session', JSON.stringify(session));
}
await post(`${process.env.UMAMI_URL}/api/collect`, {
type: 'pageview',
payload: { url, referrer, session: JSON.parse(window.localStorage.getItem('umami.session')) },
});
}
})();

+ 33
- 0
sql/schema.sql View File

@ -0,0 +1,33 @@
create table website (
website_id uuid primary key,
hostname varchar(255) unique not null,
created_at timestamp with time zone default current_timestamp
);
create table session (
session_id uuid primary key,
website_id uuid references website(website_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
browser varchar(20),
os varchar(20),
screen varchar(11),
language varchar(35),
country char(2)
);
create table pageview (
view_id serial primary key,
session_id uuid references session(session_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
url varchar(500) not null,
referrer varchar(500)
);
create table event (
event_id serial primary key,
session_id uuid references session(session_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
url varchar(500) not null,
event_type varchar(50) not null,
event_value varchar(255) not null
);

+ 3981
- 0
styles/bootstrap-grid.css
File diff suppressed because it is too large
View File


+ 25
- 0
styles/index.css View File

@ -0,0 +1,25 @@
html,
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 17px;
font-weight: 300;
line-height: 1.8;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
#__next {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}

+ 8450
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save