Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
1a9d57ed0c
27
README.md
27
README.md
@ -1,6 +1,15 @@
|
|||||||
<img src="images/logo.png" height="50" alt="Shynet logo">
|
|
||||||
|
|
||||||
Modern, privacy-friendly, and cookie-free web analytics.
|
<p align="center">
|
||||||
|
<img align="center" src="images/logo.png" height="50" alt="Shynet logo">
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Modern, privacy-friendly, and cookie-free web analytics.
|
||||||
|
<br>
|
||||||
|
<strong><a href="#installation">Getting started »</a></strong>
|
||||||
|
</p>
|
||||||
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#features">Features</a> • <a href="#roadmap">Roadmap</a></p>
|
||||||
|
</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@ -31,10 +40,10 @@ Not shown: service view, management view, session view, full service view. (You'
|
|||||||
|
|
||||||
#### Architecture
|
#### Architecture
|
||||||
|
|
||||||
* **Runs on a single machine** — Because it's so small, Shynet can easily run as a single docker container on a single small VPS.
|
* **Runs on a single machine** — Because it's so small, Shynet can easily run as a single docker container on a single small VPS
|
||||||
* **...or across a giant Kubernetes cluster** — For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO.
|
* **...or across a giant Kubernetes cluster** — For higher traffic installations, Shynet can be deployed with as many parallelized ingress nodes as needed, with Redis caching and separate backend workers for database IO
|
||||||
* **Built using Django** — Shynet is built using Django, so deploying, updating, and migrating can be done without headaches.
|
* **Built using Django** — Shynet is built using Django, so deploying, updating, and migrating can be done without headaches
|
||||||
* **Multiple users and sites** — A single Shynet instance can support multiple users, each tracking multiple different sites.
|
* **Multiple users and sites** — A single Shynet instance can support multiple users, each tracking multiple different sites
|
||||||
|
|
||||||
#### Tracking
|
#### Tracking
|
||||||
|
|
||||||
@ -61,7 +70,7 @@ Here's the information Shynet can give you about your visitors:
|
|||||||
|
|
||||||
#### Workflow
|
#### Workflow
|
||||||
* **Collaboration built-in** — Administrators can easily share services with other users, as well
|
* **Collaboration built-in** — Administrators can easily share services with other users, as well
|
||||||
* **Accounts (or not)** — Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/)).
|
* **Accounts (or not)** — Shynet has a fully featured account management workflow (powered by [Django Allauth](https://github.com/pennersr/django-allauth/))
|
||||||
|
|
||||||
## Recommendations
|
## Recommendations
|
||||||
|
|
||||||
@ -79,13 +88,13 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You can find installation instructions in our [Getting Started Guide](GUIDE.md#installation).
|
You can find installation instructions in the [Getting Started Guide](GUIDE.md#installation).
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
**Does Shynet respond to Do Not Track (DNT) signals?** Yes. While there isn't any standardized way to handle DNT requests, Shynet allows you to specify whether you want to collect any data from users with DNT enabled on a per-service basis. (By default, Shynet will _not_ collect any data from users who specify DNT.)
|
**Does Shynet respond to Do Not Track (DNT) signals?** Yes. While there isn't any standardized way to handle DNT requests, Shynet allows you to specify whether you want to collect any data from users with DNT enabled on a per-service basis. (By default, Shynet will _not_ collect any data from users who specify DNT.)
|
||||||
|
|
||||||
**Is this GDPR compliant?** It also depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
|
**Is this GDPR compliant?** It depends on how you use it. If you're worried about GDPR, you should talk to a lawyer about your particular data collection practices. I'm not a lawyer. (And this isn't legal advice.)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
@ -42,7 +42,15 @@ def _geoip2_lookup(ip):
|
|||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def ingress_request(
|
def ingress_request(
|
||||||
service_uuid, tracker, time, payload, ip, location, user_agent, dnt=False, identifier=""
|
service_uuid,
|
||||||
|
tracker,
|
||||||
|
time,
|
||||||
|
payload,
|
||||||
|
ip,
|
||||||
|
location,
|
||||||
|
user_agent,
|
||||||
|
dnt=False,
|
||||||
|
identifier="",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
|
service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
|
||||||
@ -78,7 +86,8 @@ def ingress_request(
|
|||||||
if (
|
if (
|
||||||
ua.is_bot
|
ua.is_bot
|
||||||
or (ua.browser.family or "").strip().lower() == "googlebot"
|
or (ua.browser.family or "").strip().lower() == "googlebot"
|
||||||
or (ua.device.family or ua.device.model or "").strip().lower() == "spider"
|
or (ua.device.family or ua.device.model or "").strip().lower()
|
||||||
|
== "spider"
|
||||||
):
|
):
|
||||||
device_type = "ROBOT"
|
device_type = "ROBOT"
|
||||||
elif ua.is_mobile:
|
elif ua.is_mobile:
|
||||||
|
@ -2,14 +2,15 @@ import base64
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, reverse
|
from django.shortcuts import render, reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import TemplateView, View
|
from django.views.generic import TemplateView, View
|
||||||
from django.core.cache import cache
|
|
||||||
from ipware import get_client_ip
|
from ipware import get_client_ip
|
||||||
|
|
||||||
from core.models import Service
|
from core.models import Service
|
||||||
|
|
||||||
from ..tasks import ingress_request
|
from ..tasks import ingress_request
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
import traceback
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from core.models import User
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
import uuid
|
|
||||||
import traceback
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -12,8 +14,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"hostname",
|
"hostname", type=str,
|
||||||
type=str,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
import traceback
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from core.models import User
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
import uuid
|
|
||||||
import traceback
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -12,18 +14,13 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"email",
|
"email", type=str,
|
||||||
type=str,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
email = options.get("email")
|
email = options.get("email")
|
||||||
password = get_random_string()
|
password = get_random_string()
|
||||||
User.objects.create_superuser(
|
User.objects.create_superuser(str(uuid.uuid4()), email=email, password=password)
|
||||||
str(uuid.uuid4()), email=email, password=password
|
self.stdout.write(self.style.SUCCESS("Successfully created a Shynet superuser"))
|
||||||
)
|
|
||||||
self.stdout.write(
|
|
||||||
self.style.SUCCESS("Successfully created a Shynet superuser")
|
|
||||||
)
|
|
||||||
self.stdout.write(f"Email address: {email}")
|
self.stdout.write(f"Email address: {email}")
|
||||||
self.stdout.write(f"Password: {password}")
|
self.stdout.write(f"Password: {password}")
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
import traceback
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from core.models import User
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
import uuid
|
|
||||||
import traceback
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -12,8 +14,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"name",
|
"name", type=str,
|
||||||
type=str,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('core', '0002_auto_20200415_1742'),
|
("core", "0002_auto_20200415_1742"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='service',
|
model_name="service",
|
||||||
name='respect_dnt',
|
name="respect_dnt",
|
||||||
field=models.BooleanField(default=True),
|
field=models.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
from allauth.account.admin import EmailAddress
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import Service, User
|
from core.models import Service, User
|
||||||
from allauth.account.admin import EmailAddress
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceForm(forms.ModelForm):
|
class ServiceForm(forms.ModelForm):
|
||||||
@ -27,7 +27,10 @@ class ServiceForm(forms.ModelForm):
|
|||||||
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
|
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
|
||||||
}
|
}
|
||||||
|
|
||||||
collaborators = forms.CharField(help_text="Which users should have read-only access to this service? (Comma separated list of emails.)", required=False)
|
collaborators = forms.CharField(
|
||||||
|
help_text="Which users should have read-only access to this service? (Comma separated list of emails.)",
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
def clean_collaborators(self):
|
def clean_collaborators(self):
|
||||||
collaborators = []
|
collaborators = []
|
||||||
@ -35,7 +38,9 @@ class ServiceForm(forms.ModelForm):
|
|||||||
email = collaborator_email.strip()
|
email = collaborator_email.strip()
|
||||||
if email == "":
|
if email == "":
|
||||||
continue
|
continue
|
||||||
collaborator_email_linked = EmailAddress.objects.filter(email__iexact=email).first()
|
collaborator_email_linked = EmailAddress.objects.filter(
|
||||||
|
email__iexact=email
|
||||||
|
).first()
|
||||||
if collaborator_email_linked is None:
|
if collaborator_email_linked is None:
|
||||||
raise forms.ValidationError(f"Email '{email}' is not registered")
|
raise forms.ValidationError(f"Email '{email}' is not registered")
|
||||||
collaborators.append(collaborator_email_linked.user)
|
collaborators.append(collaborator_email_linked.user)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
from datetime import datetime, time
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from datetime import time, datetime
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import pycountry
|
|
||||||
import flag
|
import flag
|
||||||
|
import pycountry
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
@ -41,12 +42,7 @@ def country_name(isocode):
|
|||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def relative_stat_tone(
|
def relative_stat_tone(
|
||||||
start,
|
start, end, good="UP", good_classes=None, bad_classes=None, neutral_classes=None,
|
||||||
end,
|
|
||||||
good="UP",
|
|
||||||
good_classes=None,
|
|
||||||
bad_classes=None,
|
|
||||||
neutral_classes=None,
|
|
||||||
):
|
):
|
||||||
good_classes = good_classes or "~positive"
|
good_classes = good_classes or "~positive"
|
||||||
bad_classes = bad_classes or "~critical"
|
bad_classes = bad_classes or "~critical"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404, reverse
|
from django.shortcuts import get_object_or_404, reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -12,7 +13,6 @@ from django.views.generic import (
|
|||||||
UpdateView,
|
UpdateView,
|
||||||
)
|
)
|
||||||
from rules.contrib.views import PermissionRequiredMixin
|
from rules.contrib.views import PermissionRequiredMixin
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from analytics.models import Session
|
from analytics.models import Session
|
||||||
from core.models import Service
|
from core.models import Service
|
||||||
|
Loading…
Reference in New Issue
Block a user