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>
 | 
			
		||||
 | 
			
		||||
@ -31,10 +40,10 @@ Not shown: service view, management view, session view, full service view. (You'
 | 
			
		||||
 | 
			
		||||
#### 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.
 | 
			
		||||
* **...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.
 | 
			
		||||
* **Multiple users and sites** — A single Shynet instance can support multiple users, each tracking multiple different sites.
 | 
			
		||||
* **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
 | 
			
		||||
* **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
 | 
			
		||||
 | 
			
		||||
#### Tracking
 | 
			
		||||
 | 
			
		||||
@ -61,7 +70,7 @@ Here's the information Shynet can give you about your visitors:
 | 
			
		||||
 | 
			
		||||
#### Workflow
 | 
			
		||||
* **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
 | 
			
		||||
 | 
			
		||||
@ -79,13 +88,13 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
 | 
			
		||||
**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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,15 @@ def _geoip2_lookup(ip):
 | 
			
		||||
 | 
			
		||||
@shared_task
 | 
			
		||||
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:
 | 
			
		||||
        service = Service.objects.get(pk=service_uuid, status=Service.ACTIVE)
 | 
			
		||||
@ -78,7 +86,8 @@ def ingress_request(
 | 
			
		||||
            if (
 | 
			
		||||
                ua.is_bot
 | 
			
		||||
                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"
 | 
			
		||||
            elif ua.is_mobile:
 | 
			
		||||
 | 
			
		||||
@ -2,14 +2,15 @@ import base64
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import render, reverse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.decorators.csrf import csrf_exempt
 | 
			
		||||
from django.views.generic import TemplateView, View
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
 | 
			
		||||
from core.models import Service
 | 
			
		||||
 | 
			
		||||
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.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
 | 
			
		||||
import uuid
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from core.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@ -12,8 +14,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "hostname",
 | 
			
		||||
            type=str,
 | 
			
		||||
            "hostname", type=str,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
@ -24,4 +25,4 @@ class Command(BaseCommand):
 | 
			
		||||
            self.style.SUCCESS(
 | 
			
		||||
                f"Successfully set the hostname to '{options.get('hostname')}'"
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandError
 | 
			
		||||
import traceback
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
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
 | 
			
		||||
import uuid
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from core.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@ -12,18 +14,13 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "email",
 | 
			
		||||
            type=str,
 | 
			
		||||
            "email", type=str,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        email = options.get("email")
 | 
			
		||||
        password = get_random_string()
 | 
			
		||||
        User.objects.create_superuser(
 | 
			
		||||
            str(uuid.uuid4()), email=email, password=password
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write(
 | 
			
		||||
            self.style.SUCCESS("Successfully created a Shynet superuser")
 | 
			
		||||
        )
 | 
			
		||||
        User.objects.create_superuser(str(uuid.uuid4()), email=email, password=password)
 | 
			
		||||
        self.stdout.write(self.style.SUCCESS("Successfully created a Shynet superuser"))
 | 
			
		||||
        self.stdout.write(f"Email address: {email}")
 | 
			
		||||
        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.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
 | 
			
		||||
import uuid
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from core.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@ -12,8 +14,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "name",
 | 
			
		||||
            type=str,
 | 
			
		||||
            "name", type=str,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
@ -24,4 +25,4 @@ class Command(BaseCommand):
 | 
			
		||||
            self.style.SUCCESS(
 | 
			
		||||
                f"Successfully set the whitelabel to '{options.get('name')}'"
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -6,13 +6,13 @@ from django.db import migrations, models
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0002_auto_20200415_1742'),
 | 
			
		||||
        ("core", "0002_auto_20200415_1742"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='service',
 | 
			
		||||
            name='respect_dnt',
 | 
			
		||||
            model_name="service",
 | 
			
		||||
            name="respect_dnt",
 | 
			
		||||
            field=models.BooleanField(default=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
from allauth.account.admin import EmailAddress
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from core.models import Service, User
 | 
			
		||||
from allauth.account.admin import EmailAddress
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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?",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    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):
 | 
			
		||||
        collaborators = []
 | 
			
		||||
@ -35,7 +38,9 @@ class ServiceForm(forms.ModelForm):
 | 
			
		||||
            email = collaborator_email.strip()
 | 
			
		||||
            if email == "":
 | 
			
		||||
                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:
 | 
			
		||||
                raise forms.ValidationError(f"Email '{email}' is not registered")
 | 
			
		||||
            collaborators.append(collaborator_email_linked.user)
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
from datetime import datetime, time
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
from datetime import time, datetime
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
import pycountry
 | 
			
		||||
 | 
			
		||||
import flag
 | 
			
		||||
import pycountry
 | 
			
		||||
from django import template
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.html import escape
 | 
			
		||||
@ -41,17 +42,12 @@ def country_name(isocode):
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def relative_stat_tone(
 | 
			
		||||
    start,
 | 
			
		||||
    end,
 | 
			
		||||
    good="UP",
 | 
			
		||||
    good_classes=None,
 | 
			
		||||
    bad_classes=None,
 | 
			
		||||
    neutral_classes=None,
 | 
			
		||||
    start, end, good="UP", good_classes=None, bad_classes=None, neutral_classes=None,
 | 
			
		||||
):
 | 
			
		||||
    good_classes = good_classes or "~positive"
 | 
			
		||||
    bad_classes = bad_classes or "~critical"
 | 
			
		||||
    neutral_classes = neutral_classes or "~neutral"
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    if start == None or end == None or start == end:
 | 
			
		||||
        return neutral_classes
 | 
			
		||||
    if good == "UP":
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.shortcuts import get_object_or_404, reverse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
@ -12,7 +13,6 @@ from django.views.generic import (
 | 
			
		||||
    UpdateView,
 | 
			
		||||
)
 | 
			
		||||
from rules.contrib.views import PermissionRequiredMixin
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
 | 
			
		||||
from analytics.models import Session
 | 
			
		||||
from core.models import Service
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user