Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
23f1fdbb3f
30
GUIDE.md
30
GUIDE.md
@ -1,11 +1,4 @@
|
|||||||
<p align="center">
|
# Getting Started
|
||||||
<h3 align="center">🔭 Shynet 🔭</h3>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
Web analytics that's self hosted, cookie free, privacy friendly, and useful(?)
|
|
||||||
<br>
|
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@ -50,6 +43,8 @@ DB_PORT=<your db port>
|
|||||||
DJANGO_SECRET_KEY=<your Django secret key; just a random string>
|
DJANGO_SECRET_KEY=<your Django secret key; just a random string>
|
||||||
# Don't leak error details to visitors, very important
|
# Don't leak error details to visitors, very important
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
# Unless you are using an external Celery task queue, make sure this
|
||||||
|
# is set to True.
|
||||||
CELERY_TASK_ALWAYS_EAGER=True
|
CELERY_TASK_ALWAYS_EAGER=True
|
||||||
# For better security, set this to your deployment's domain. Comma separated.
|
# For better security, set this to your deployment's domain. Comma separated.
|
||||||
ALLOWED_HOSTS=*
|
ALLOWED_HOSTS=*
|
||||||
@ -74,6 +69,14 @@ SERVER_EMAIL=Shynet <noreply@shynet.example.com>
|
|||||||
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
||||||
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False
|
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False
|
||||||
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||||
|
|
||||||
|
# Other Shynet settings
|
||||||
|
# How frequently should the monitoring script "phone home" (in ms)?
|
||||||
|
SCRIPT_HEARTBEAT_FREQUENCY=5000
|
||||||
|
# Should only superusers (admins) be able to create services? This is helpful
|
||||||
|
# when you'd like to invite others to your Shynet instance but don't want
|
||||||
|
# them to be able to create services of their own.
|
||||||
|
ONLY_SUPERUSERS_CREATE=False
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py migrate`.
|
4. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py migrate`.
|
||||||
@ -138,6 +141,8 @@ DB_PORT=<your db port>
|
|||||||
DJANGO_SECRET_KEY=<your Django secret key; just a random string>
|
DJANGO_SECRET_KEY=<your Django secret key; just a random string>
|
||||||
# Don't leak error details to visitors, very important
|
# Don't leak error details to visitors, very important
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
# Unless you are using an external Celery task queue, make sure this
|
||||||
|
# is set to True.
|
||||||
CELERY_TASK_ALWAYS_EAGER=True
|
CELERY_TASK_ALWAYS_EAGER=True
|
||||||
# For better security, set this to your deployment's domain. Comma separated.
|
# For better security, set this to your deployment's domain. Comma separated.
|
||||||
ALLOWED_HOSTS=*
|
ALLOWED_HOSTS=*
|
||||||
@ -162,6 +167,15 @@ SERVER_EMAIL=Shynet <noreply@shynet.example.com>
|
|||||||
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
||||||
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False
|
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False
|
||||||
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||||
|
|
||||||
|
|
||||||
|
# Other Shynet settings
|
||||||
|
# How frequently should the monitoring script "phone home" (in ms)?
|
||||||
|
SCRIPT_HEARTBEAT_FREQUENCY=5000
|
||||||
|
# Should only superusers (admins) be able to create services? This is helpful
|
||||||
|
# when you'd like to invite others to your Shynet instance but don't want
|
||||||
|
# them to be able to create services of their own.
|
||||||
|
ONLY_SUPERUSERS_CREATE=False
|
||||||
```
|
```
|
||||||
|
|
||||||
9. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest-ssl python manage.py migrate`.
|
9. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest-ssl python manage.py migrate`.
|
||||||
|
18
README.md
18
README.md
@ -1,14 +1,8 @@
|
|||||||
<p align="center">
|
<img src="images/logo.png" height="50" alt="Shynet logo">
|
||||||
<h3 align="center">🔭 Shynet 🔭</h3>
|
|
||||||
|
|
||||||
<p align="center">
|
Modern, privacy-friendly, and cookie-free web analytics.
|
||||||
Web analytics that's self hosted, cookie free, privacy friendly, and useful(?)
|
|
||||||
<br>
|
<br>
|
||||||
<br>
|
|
||||||
<a href="#installation"><strong>Getting started »</strong></a>
|
|
||||||
<br>
|
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
@ -85,7 +79,7 @@ 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 our [Getting Started Guide](GUIDE.md#installation).
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
@ -118,4 +112,4 @@ Shynet is made available under the [Apache License, version 2.0](LICENSE).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
a17t was created by [Miles McCain](https://miles.land) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).
|
Shynet was created by [Miles McCain](https://miles.land) ([@MilesMcCain](https://twitter.com/MilesMcCain)) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).
|
||||||
|
BIN
images/logo.png
Normal file
BIN
images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -1,3 +1,10 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
|
||||||
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--family-primary: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--family-secondary: var(--family-primary);
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,6 +4,9 @@ window.onload = function () {
|
|||||||
Math.random().toString(36).substring(2, 15);
|
Math.random().toString(36).substring(2, 15);
|
||||||
function sendUpdate() {
|
function sendUpdate() {
|
||||||
try {
|
try {
|
||||||
|
if (document.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open(
|
xhr.open(
|
||||||
"POST",
|
"POST",
|
||||||
@ -21,8 +24,8 @@ window.onload = function () {
|
|||||||
window.performance.timing.navigationStart,
|
window.performance.timing.navigationStart,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
setInterval(sendUpdate, 5000);
|
setInterval(sendUpdate, parseInt("{{heartbeat_frequency}}"));
|
||||||
sendUpdate();
|
sendUpdate();
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,9 @@ 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 ..tasks import ingress_request
|
from ..tasks import ingress_request
|
||||||
|
|
||||||
@ -58,8 +60,15 @@ class PixelView(View):
|
|||||||
@method_decorator(csrf_exempt, name="dispatch")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
class ScriptView(View):
|
class ScriptView(View):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
service_uuid = self.kwargs.get("service_uuid")
|
||||||
|
origins = cache.get(f"service_origins_{service_uuid}")
|
||||||
|
if origins is None:
|
||||||
|
service = Service.objects.get(uuid=service_uuid)
|
||||||
|
origins = service.origins
|
||||||
|
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
|
||||||
|
|
||||||
resp = super().dispatch(request, *args, **kwargs)
|
resp = super().dispatch(request, *args, **kwargs)
|
||||||
resp["Access-Control-Allow-Origin"] = "*"
|
resp["Access-Control-Allow-Origin"] = origins
|
||||||
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
|
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
|
||||||
resp[
|
resp[
|
||||||
"Access-Control-Allow-Headers"
|
"Access-Control-Allow-Headers"
|
||||||
@ -82,10 +91,15 @@ class ScriptView(View):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
heartbeat_frequency = settings.SCRIPT_HEARTBEAT_FREQUENCY
|
||||||
return render(
|
return render(
|
||||||
self.request,
|
self.request,
|
||||||
"analytics/scripts/page.js",
|
"analytics/scripts/page.js",
|
||||||
context={"endpoint": endpoint, "protocol": protocol},
|
context={
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"protocol": protocol,
|
||||||
|
"heartbeat_frequency": heartbeat_frequency,
|
||||||
|
},
|
||||||
content_type="application/javascript",
|
content_type="application/javascript",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from allauth.account.admin import EmailAddress
|
|||||||
class ServiceForm(forms.ModelForm):
|
class ServiceForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ["name", "link", "respect_dnt", "collaborators"]
|
fields = ["name", "link", "respect_dnt", "origins", "collaborators"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
"origins": forms.TextInput(),
|
"origins": forms.TextInput(),
|
||||||
@ -29,7 +29,6 @@ class ServiceForm(forms.ModelForm):
|
|||||||
|
|
||||||
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 = []
|
||||||
for collaborator_email in self.cleaned_data["collaborators"].split(","):
|
for collaborator_email in self.cleaned_data["collaborators"].split(","):
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
{% load a17t_tags %}
|
||||||
|
|
||||||
|
{{form.name|a17t}}
|
||||||
|
{{form.link|a17t}}
|
||||||
|
{{form.collaborators|a17t}}
|
||||||
|
|
||||||
|
<details class="p-4 border rounded">
|
||||||
|
<summary class="cursor-pointer text-sm">Advanced settings</summary>
|
||||||
|
<hr class="sep h-4">
|
||||||
|
{{form.respect_dnt|a17t}}
|
||||||
|
{{form.origins|a17t}}
|
||||||
|
</details>
|
@ -10,7 +10,7 @@
|
|||||||
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
|
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
{{form|a17t}}
|
{% include 'dashboard/includes/service_form.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="section ~urge !normal p-4">
|
<div class="section ~urge !normal p-4">
|
||||||
<button type="submit" class="button ~urge !high">Create</button>
|
<button type="submit" class="button ~urge !high">Create</button>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<form class="card ~neutral !low p-0" method="POST">
|
<form class="card ~neutral !low p-0" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
{{form|a17t}}
|
{% include 'dashboard/includes/service_form.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="section ~neutral !normal p-4 flex justify-between">
|
<div class="section ~neutral !normal p-4 flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
@ -12,6 +12,7 @@ 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
|
||||||
@ -77,6 +78,13 @@ class ServiceUpdateView(
|
|||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("dashboard:service", kwargs={"pk": self.object.uuid})
|
return reverse("dashboard:service", kwargs={"pk": self.object.uuid})
|
||||||
|
|
||||||
|
def form_valid(self, *args, **kwargs):
|
||||||
|
resp = super().form_valid(*args, **kwargs)
|
||||||
|
cache.set(
|
||||||
|
f"service_origins_{self.object.uuid}", self.object.origins, timeout=3600
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
class ServiceDeleteView(
|
class ServiceDeleteView(
|
||||||
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView
|
||||||
|
@ -256,3 +256,7 @@ ONLY_SUPERUSERS_CREATE = os.getenv("ONLY_SUPERUSERS_CREATE", "True") == "True"
|
|||||||
# Should the script use HTTPS to send the POST requests? The hostname is from
|
# Should the script use HTTPS to send the POST requests? The hostname is from
|
||||||
# the django SITE default. (Edit it using the admin panel.)
|
# the django SITE default. (Edit it using the admin panel.)
|
||||||
SCRIPT_USE_HTTPS = os.getenv("SCRIPT_USE_HTTPS", "True") == "True"
|
SCRIPT_USE_HTTPS = os.getenv("SCRIPT_USE_HTTPS", "True") == "True"
|
||||||
|
|
||||||
|
# How frequently should the tracking script "phone home" with a heartbeat, in
|
||||||
|
# milliseconds?
|
||||||
|
SCRIPT_HEARTBEAT_FREQUENCY = int(os.getenv("SCRIPT_HEARTBEAT_FREQUENCY", "5000"))
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript><img src="//localhost:8000/ingress/211410e6-b401-4fe8-8740-7926368590be/pixel.gif"></noscript>
|
<noscript><img src="//localhost:8000/ingress/test_uuid/pixel.gif"></noscript>
|
||||||
<script src="//localhost:8000/ingress/211410e6-b401-4fe8-8740-7926368590be/script.js"></script>
|
<script src="//localhost:8000/ingress/test_uuid/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user