OpenPanel — otwarta alternatywa dla Mixpanela z session replay
Analityka produktowa i webowa w jednym, na własnym serwerze: lejki, kohorty, profile użytkowników, testy A/B i nagrania sesji bez limitu. Rozbieramy mechanizm identyfikacji bez ciasteczek (sól rotowana co dobę), maskowanie w nagraniach sesji, kolejkowanie zdarzeń do momentu zgody i proxy omijające adblockery. Do tego pełna instrukcja wdrożenia w Dockerze, tracking z Laravela przez HTTP API — bo SDK dla PHP nie ma — i rachunek z AGPL-3.0 oraz z modelem, w którym najnowsze obrazy kosztują 20 USD miesięcznie.
Wybór narzędzia do analityki sprowadza się zwykle do trzech niesatysfakcjonujących opcji. Google Analytics 4 jest darmowe i płaci się za nie inaczej: dane siedzą u Google'a, dostęp do surowych zdarzeń praktycznie wymaga BigQuery, a w kilku krajach Unii narzędzie zebrało zakazy z powodu transferu danych do Stanów. Plausible jest prywatne, lekkie i nie odpowie na pytanie „ilu użytkowników, którzy zarejestrowali się w marcu, użyło funkcji eksportu w ciągu pierwszego tygodnia". Mixpanel odpowie na to pytanie i policzy sobie za to według cennika, który przy rosnącym ruchu przestaje być zabawny — a nagrania sesji ma ograniczone do pięciu tysięcy miesięcznie w planie darmowym.
OpenPanel (github.com/Openpanel-dev/openpanel) celuje dokładnie w tę lukę: analityka produktowa z lejkami i kohortami, jak w Mixpanelu, z prostotą wdrożenia bliższą Plausible, na własnym serwerze i na licencji open source. Poniżej: co realnie dostajemy, jak to postawić, jak działa identyfikacja bez ciasteczek (mechanizm jest nieoczywisty i ma konsekwencje dla tego, o co można pytać dane), jak podłączyć to do aplikacji w Laravelu — bo SDK dla PHP nie istnieje — i gdzie są koszty, o których strona projektu nie mówi wprost.
Stan projektu
Dane z API GitHuba na 13 września 2026:
- 6904 gwiazdki i 469 forków — projekt młody, ale z wyraźnym zainteresowaniem,
- repozytorium założone 29 lutego 2024, kod w TypeScripcie, ostatni commit z 4 września 2026,
- 67 commitów w ostatnim miesiącu i 47 otwartych zgłoszeń — rozwój aktywny, skala utrzymania mała,
- licencja AGPL-3.0, do której wracamy na końcu, bo przy komercyjnym użyciu ma znaczenie,
- zero wydań na GitHubie. Nie ma tagów wersji ani listy zmian w formie, do której jesteśmy przyzwyczajeni — wersjonowanie odbywa się przez obrazy Dockera i osobną gałąź
self-hosting. To pierwszy sygnał, że mamy do czynienia z projektem prowadzonym jak produkt SaaS, który dodatkowo daje się postawić u siebie, a nie z klasycznym projektem open source.
Za projektem stoi Carl Lindesvärd, a społeczność zbiera się na Discordzie. Jest hostowana wersja komercyjna pod openpanel.dev i to ona finansuje rozwój.
Co dostajemy
Lista funkcji z README jest długa, więc warto oddzielić to, co realnie różnicuje narzędzie, od tego, co ma każdy:
- Analityka produktowa — lejki, kohorty, profile użytkowników i historia sesji. To jest ta część, której Plausible nie ma i po którą sięga się do Mixpanela,
- Nagrania sesji na bazie rrweb, bez limitu liczby nagrań, z retencją 30 dni. README porównuje to wprost z Mixpanelem, gdzie limit to pięć tysięcy sesji miesięcznie w planie darmowym i dwadzieścia tysięcy w płatnym,
- Testy A/B z podziałem wyników na warianty — wbudowane, nie przez osobne narzędzie,
- Tracking przychodów — zakupy, subskrypcje i LTV obok zdarzeń produktowych,
- Powiadomienia oparte o zdarzenia i lejki,
- Tracking bez ciasteczek jako zachowanie domyślne, nie opcja,
- Serwer MCP z 38 narzędziami, hostowany, bez instalacji — pozwala pytać o dane z Claude'a, Cursora albo dowolnego klienta MCP,
- Asystent AI w panelu — opcjonalny, na kluczu do OpenAI albo Anthropica,
- SDK dla webu, Reacta, Next.js, Nuxta, Vue, Astro, Express, Pythona, Ruby, Rusta, Javy, Kotlina, Swifta i React Native.
Tej listy brakuje jednej pozycji, którą trzeba wypunktować od razu, bo dla nas jest kluczowa: nie ma SDK dla PHP. Nie jest to blokada — HTTP API jest udokumentowane i wywołanie go z Laravela to kilkanaście linii — ale trzeba to policzyć w kosztach wdrożenia. Wracamy do tego w rozdziale o naszym stacku.
Co siedzi pod maską
Stack z README wygląda tak: Next.js na panel, Fastify na API zdarzeń, PostgreSQL na podstawowe informacje, ClickHouse na zdarzenia, Redis na cache, pub/sub i kolejkę, plus BullMQ i GroupMQ jako mechanizm kolejek. Do tego tRPC, Tailwind, shadcn/ui, Arctic i Oslo na uwierzytelnianie, Resend na pocztę.
Wniosek operacyjny z tej listy jest ważniejszy niż sama lista: to nie jest jednokontenerowa aplikacja. Wdrożenie oznacza utrzymywanie trzech systemów stanowych — Postgresa, ClickHouse'a i Redisa — z których ClickHouse jest najmniej znany typowemu zespołowi webowemu i najmniej wybaczający przy błędnej konfiguracji dysku. Backup obejmuje trzy różne mechanizmy. Aktualizacja obejmuje migracje w dwóch bazach.
Wybór między instancją hostowaną a własną nie jest tu wyborem między „płacić" i „nie płacić". Jest wyborem między rachunkiem od dostawcy a czasem zespołu na utrzymanie czterech usług stanowych, w tym bazy kolumnowej. Przy małym ruchu i bez dyżuru pierwsza opcja bywa tańsza w rzeczywistych pieniądzach.
Wdrożenie na własnym serwerze
Instalacja jest zaskakująco dopracowana — projekt ma dedykowaną gałąź z gotowym zestawem skryptów. Uwaga na gałąź: self-hosting, nie main.
git clone -b self-hosting https://github.com/Openpanel-dev/openpanel && cd openpanel/self-hosting
./setup
./startSkrypt setup robi trzy rzeczy: instaluje Node (jeśli się zgodzimy), instaluje Dockera (jeśli się zgodzimy) i uruchamia interaktywny kwestionariusz konfigurujący instancję. Dokumentacja podaje czas 30 sekund do 2 minut i zastrzega, że testowano to na Ubuntu 24.04. Gdy ./setup zawiedzie, ręczna droga to zainstalowanie Dockera, Node'a i npm-a oraz odpalenie npm run quiz w katalogu self-hosting.
Zestaw skryptów pomocniczych jest kompletny i warto go znać przed pierwszą awarią:
./start # start wszystkich usług
./stop # zatrzymanie
./logs # logi na żywo ze wszystkich usług
./rebuild op-dashboard # przebudowa i restart jednej usługi
./update # aktualizacja do najnowszej wersji
./danger_wipe_everything # usuwa kontenery, wolumeny i WSZYSTKIE daneNazwa ostatniego skryptu jest uczciwa i należy ją traktować literalnie.
Poza Docker Compose dokumentacja ma osobne przewodniki wdrożenia na Coolify, Dokploy i Kubernetes, plus gotowy coolify.yml w repozytorium. Przy istniejącej platformie PaaS to skraca drogę do kilku kliknięć.
Konfiguracja, o której trzeba pamiętać
Poczta. Reset hasła i zaproszenia wymagają maila. Domyślnie projekt używa Resend (RESEND_API_KEY), ale przyjmuje też własny SMTP:
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=smtp-user@example.com
SMTP_PASS=haslo
EMAIL_SENDER=noreply@yourdomain.comSMTP_HOST ma pierwszeństwo nad RESEND_API_KEY, jeśli ustawione są oba. A jeśli nie ustawimy żadnego — maile są logowane do konsoli zamiast wysyłane. Cicho, bez błędu. To dokładnie ten rodzaj zachowania, który odkrywa się w dniu, w którym ktoś zapomniał hasła.
Rejestracja. Po utworzeniu pierwszego użytkownika rejestracje są domyślnie wyłączone — sensowna decyzja dla instancji wystawionej do internetu. Zmienia to ALLOW_REGISTRATION=true, a zaproszenia wyłącza ALLOW_INVITATION=false.
Zarządzany Redis. Jeśli używamy Redisa jako usługi zewnętrznej, trzeba ręcznie ustawić notify-keyspace-events. Bez tego OpenPanel nie nasłuchuje wygasających kluczy, a na nich opiera się licznik aktywnych odwiedzających. Dokumentacja obiecuje ostrzeżenie w logach, ale objaw jest subtelny: wszystko działa, tylko liczba osób „teraz na stronie" jest zawsze zerowa.
Adres API w SDK. Przy własnej instancji trzeba jawnie podać apiUrl — ścieżka /api na naszej domenie. Zapomnienie o tym kończy się wysyłaniem zdarzeń do instancji publicznej, gdzie nasz identyfikator klienta nic nie znaczy.
Asystent AI. Opcjonalny; obsługuje OpenAI i Anthropica, a wybieralne modele zależą od tego, który klucz ustawimy. Bez klucza panel czatu nadal się otwiera i pokazuje podpowiedź konfiguracyjną — reszta narzędzia działa normalnie. Dokumentacja ostrzega wprost, że funkcje AI generują koszty po stronie dostawcy modelu.
Wersje obrazów i model wspierania projektu
To jest szczegół, który zmienia ocenę projektu i o którym łatwo się nie dowiedzieć, dopóki nie trafi się na odpowiednią stronę dokumentacji. OpenPanel buduje obrazy Dockera z każdego commita, ale trzyma je w prywatnym rejestrze. Dostęp do nich mają wspierający, od 20 USD miesięcznie — dostają klucz API do proxy pod docker.openpanel.dev:
echo "twoj_klucz_api" | docker login docker.openpanel.dev -u user --password-stdin
./get_latest_images apply
./restartPubliczne obrazy istnieją, ale są starsze — dokumentacja mówi o dostępie do funkcji „tygodnie albo miesiące przed stabilnymi wydaniami". Trzy rzeczy warto tu powiedzieć wprost:
- To jest w porządku licencyjnie i sensowne biznesowo. Kod jest dostępny na AGPL, więc obrazy można zbudować samodzielnie z repozytorium — płaci się za wygodę i za utrzymanie projektu, nie za dostęp do kodu,
- Ale zmienia to kalkulację. Przy self-hostingu „darmowym" trzeba się liczyć albo z opóźnieniem względem rozwoju, albo z własnym pipeline'em budującym obrazy, albo z 20 USD miesięcznie. Trzecia opcja jest najtańsza i warto ją po prostu wziąć,
- Aktualizacja to nie tylko
./update. Dokumentacja ma osobną stronę z listą zmian dla self-hostingu i każe ją czytać przed aktualizacją oraz stosować opisane tam zmiany do własnej instancji. Przy braku wydań na GitHubie to jedyne miejsce, w którym dowiemy się o zmianie łamiącej zgodność.
Skalowanie
Domyślna konfiguracja Compose wystarcza do większości wdrożeń. Przy tysiącach zdarzeń na sekundę i wielu replikach workera pojawia się konkretny problem: każda replika API i workera otwiera własną pulę połączeń do Postgresa, a ten ma twardy limit — pojawiają się błędy too many connections.
Odpowiedzią jest PGBouncer przed Postgresem. Dokumentacja podaje gotową definicję usługi (edoburu/pgbouncer, POOL_MODE=transaction, MAX_CLIENT_CONN=1000, DEFAULT_POOL_SIZE=20), przełączenie zależności op-api i op-worker na pooler oraz jedną rzecz, o której łatwo zapomnieć: Prisma musi wiedzieć, że rozmawia z poolerem, więc do DATABASE_URL dochodzi &pgbouncer=true.
Identyfikacja bez ciasteczek
Ta część jest najciekawsza technicznie i najbardziej konsekwentna dla tego, o co można pytać zebrane dane. OpenPanel nie stawia ciasteczek. Identyfikator urządzenia liczy jako skrót z czterech składników:
generateDeviceId({ salt, ua, ip, origin })
// createHash(`${ua}:${ip}:${origin}:${salt}`, 16)Czyli user agent, adres IP, identyfikator projektu i sól, która rotuje codziennie o północy UTC. System trzyma sól bieżącą i wczorajszą, żeby obsłużyć zdarzenia przychodzące chwilę po zmianie. Konsekwencje:
- Identyfikator urządzenia jest stały w obrębie doby i resetuje się każdego dnia. To realna anonimizacja, nie deklaracja — po rotacji nie ma technicznej możliwości powiązania ruchu z poprzednim dniem na poziomie urządzenia,
- Analityka dłuższa niż jeden dzień wymaga
identify(). Pytanie „ilu użytkowników wróciło po tygodniu" da się zadać tylko dla użytkowników zidentyfikowanych własnymprofileId. Dla ruchu anonimowego jest ono technicznie niewykonalne i trzeba to wiedzieć przed obiecaniem komukolwiek raportu retencji, - Zmiana IP zmienia identyfikator. Przejście z wi-fi na sieć komórkową w połowie sesji tworzy nowe urządzenie. Przy ruchu mobilnym to zauważalne zniekształcenie.
Sesja to okres aktywności z 30-minutowym limitem bezczynności, odświeżanym każdym zdarzeniem. Profil jest identyfikatorem trwałym, przekazywanym przez nas; jeśli go nie podamy, profileId przyjmuje wartość deviceId. Innymi słowy: użytkownik anonimowy jest profilem jednodniowym.
Zdarzenia klienckie a serwerowe
OpenPanel rozróżnia zdarzenia klienckie i serwerowe na podstawie nagłówka User-Agent. Wzorce w rodzaju Go-http-client/1.0 albo node-fetch/1.0 — ogólnie „nazwa/wersja" — są rozpoznawane jako serwerowe. Różnica jest zasadnicza:
- zdarzenia klienckie tworzą sesje i generują identyfikatory urządzeń,
- zdarzenia serwerowe nie tworzą sesji — są dołączane do istniejącej sesji, jeśli taka jest, albo trafiają bez sesji.
Dokumentacja mówi to wprost: jeśli trackujemy wyłącznie z backendu, żadne sesje nie powstaną. Dla aplikacji w Laravelu, w której naturalnym odruchem jest wysyłanie zdarzeń z kolejki, to jest najważniejsza informacja w całym artykule.
Do tego dochodzą reguły znaczników czasu. Własny czas zdarzenia przekazuje się właściwością __timestamp, ale walidacja jest ostra: znacznik ponad minutę w przyszłości jest nadpisywany czasem serwera, a znacznik starszy niż 15 minut oznacza zdarzenie jako pochodzące z przeszłości — i takie zdarzenie nie utworzy nowej sesji. Mechanizm chroni przed tym, żeby import historyczny nie tworzył sztucznych sesji, ale ma skutek uboczny dla nas: zadanie kolejkowe, które poleżało 20 minut, wysyła zdarzenie niezdolne do utworzenia sesji. Przy zdarzeniach istotnych dla lejków warto więc wysyłać synchronicznie albo na kolejce o wysokim priorytecie.
Nagrania sesji
Nagrania stoją na rrweb i nie są wideo — zapisują mutacje DOM, ruchy myszy, przewijanie i interakcje jako dane strukturalne. Dwie decyzje projektowe zasługują na uznanie:
Funkcja jest domyślnie wyłączona, per projekt. Gdy jest wyłączona, skrypt nagrywania (op1-replay.js) nie jest w ogóle pobierany — główny skrypt trackujący pozostaje lekki. W wariancie npm moduł nagrywania jest dynamicznym importem, który bundler wydziela do osobnego kawałka, więc nie wchodzi do głównego bundle'a.
Maskowanie jest domyślnie włączone. I maskAllInputs, i maskAllText mają domyślną wartość true, a maskowanie zachodzi w przeglądarce, przed wysłaniem danych na serwer. Tekst jest zamieniany na ***, więc nagranie nie może przypadkowo złapać imienia ani adresu e-mail widocznego na stronie.
sessionReplay: {
enabled: true,
// domyślnie wszystko zamaskowane; odmaskowujemy wybiórczo
unmaskTextSelector: '.replay-safe, nav, footer',
// te elementy nie są nagrywane wcale — placeholder w ich miejsce
blockSelector: '.payment-form, .user-avatar',
// widoczne w nagraniu, ale bez rejestrowania interakcji
ignoreSelector: '.debug-panel',
}Pozostałe opcje sterują transportem: flushIntervalMs (domyślnie 10 000 ms), maxEventsPerChunk (200) i maxPayloadBytes (1 MB). Domyślnym selektorem blokującym jest atrybut data-openpanel-replay-block, więc najprostsza droga do wyłączenia fragmentu strony z nagrywania to dopisanie tego atrybutu do kontenera.
Wyłączenie maskAllText przełącza tryb na maskowanie wybiórcze — maskowane są wtedy tylko elementy z data-openpanel-replay-mask. Dokumentacja opatruje to jawnym ostrzeżeniem i formułuje granicę odpowiedzialności bez ogródek: to my odpowiadamy za zgodność użycia nagrań z obowiązującym prawem ochrony prywatności. Trudno się nie zgodzić — nagranie sesji z odmaskowanym tekstem jest przetwarzaniem danych osobowych na skalę, której nie widać z panelu.
Limity produktu: brak ograniczenia liczby nagrań, retencja 30 dni. Przy własnej instancji retencja jest ostatecznie kwestią naszej konfiguracji ClickHouse'a, ale wartość domyślna wynikająca z produktu to 30 dni.
Zgoda użytkownika
To najlepiej rozwiązany element całego SDK i dokładnie to, czego potrzebuje warstwa zgody na ciasteczka. Inicjalizacja z disabled: true powoduje, że nic nie jest wysyłane — wywołania track, identify, screenView i fragmenty nagrań trafiają do kolejki w pamięci. Po zgodzie jedno wywołanie ready() opróżnia całą kolejkę:
const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
apiUrl: 'https://nasza-domena.pl/api',
trackScreenViews: true,
disabled: true, // nic nie wychodzi do wywołania ready()
sessionReplay: { enabled: true },
});
// po akceptacji bannera zgody:
op.ready();Jeśli użytkownik odmówi, po prostu nie wołamy ready() — kolejka jest odrzucana przy zamknięciu strony. Detal, który czyni to rozwiązanie lepszym od typowego „załaduj skrypt po zgodzie": rekorder nagrań startuje od razu przy ładowaniu strony, ale nic nie wysyła. Po zgodzie mamy więc nagranie od pierwszej sekundy, a nie od momentu klika w banner. Bez zgody nie wychodzi ani bajt.
Jest też wariant waitForProfile: true, kolejkujący zdarzenia do momentu wywołania identify() z identyfikatorem profilu. Rozwiązuje problem zdarzeń wysyłanych, zanim aplikacja wie, kto jest zalogowany.
Adblockery i proxy przez własną domenę
Dokumentacja projektu podchodzi do tego bez udawania: adblockery blokują skrypty analityczne, generyczne listy blokad łapią wszystkie narzędzia bez rozróżnienia, a jedynym skutecznym rozwiązaniem jest przekazywanie zdarzeń przez własną domenę. Żądania pierwszej strony trudno zablokować, nie psując samej witryny.
Wbudowaną obsługę proxy mają dziś tylko SDK dla Next.js (przez route handler) i wtyczka do WordPressa. Dla każdego innego frameworka trzeba to napisać — i tu wchodzi nasz stack.
W stacku Laravela
Trzy elementy trzeba złożyć samodzielnie: proxy, tracking po stronie serwera i inicjalizację po stronie frontu.
1. Proxy w Laravelu
Dwie trasy: jedna podająca skrypt z naszej domeny, druga przekazująca zdarzenia do API. Kluczowe jest przekazanie adresu IP i nagłówka User-Agent, bo bez nich OpenPanel nie ustali ani geolokalizacji, ani urządzenia:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
class OpenPanelProxyController extends Controller
{
public function script(): Response
{
$body = Http::get('https://openpanel.dev/op1.js')->throw()->body();
return response($body, 200, [
'Content-Type' => 'text/javascript',
'Cache-Control' => 'public, max-age=86400, stale-while-revalidate=86400',
]);
}
public function track(Request $request): Response
{
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Origin' => (string) $request->header('Origin'),
'User-Agent' => (string) $request->userAgent(),
'openpanel-client-id' => (string) $request->header('openpanel-client-id'),
'openpanel-client-ip' => (string) $request->ip(),
])->post(config('services.openpanel.api').'/track', $request->all());
return response($response->body(), $response->status());
}
}Uwaga na $request->ip(): za reverse proxy albo CDN-em zwróci adres proxy, o ile nie mamy poprawnie skonfigurowanego TrustProxies. Dokumentacja OpenPanela wprost każe wybrać właściwy nagłówek zależnie od infrastruktury (cf-connecting-ip, x-forwarded-for) i to jest ten sam problem, tylko widziany od strony Laravela. Błędny IP oznacza tu nie tylko złą geolokalizację, ale też błędne identyfikatory urządzeń — bo IP wchodzi do skrótu.
2. Zdarzenia z backendu przez HTTP API
Bez SDK dla PHP uderzamy w API bezpośrednio. Uwierzytelnianie to dwa nagłówki, a endpoint /track przyjmuje pole type decydujące o rodzaju zapisu: track, identify, increment, decrement, group i assign_group.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class OpenPanel
{
public function track(string $name, string $profileId, array $properties = []): void
{
Http::withHeaders([
'openpanel-client-id' => config('services.openpanel.client_id'),
'openpanel-client-secret' => config('services.openpanel.client_secret'),
])
->timeout(3)
->post(config('services.openpanel.api').'/track', [
'type' => 'track',
'payload' => [
'name' => $name,
'profileId' => $profileId,
'properties' => $properties,
],
]);
}
}Cztery rzeczy, które trzeba tu przemyśleć, zanim to wejdzie na produkcję:
- Te zdarzenia nie utworzą sesji. User-Agent klienta HTTP Laravela zostanie rozpoznany jako serwerowy. Do lejków opartych o sesje trzeba więc trackować z przeglądarki, a backend zostawić dla zdarzeń, które sesji nie potrzebują — płatność zaksięgowana, subskrypcja odnowiona, konto usunięte,
- Kolejkuj, ale nie za długo. Wywołanie HTTP w cyklu żądania to dodane opóźnienie i punkt awarii, więc naturalnym odruchem jest zadanie kolejkowe. Pamiętaj o regule 15 minut — zdarzenie z opóźnionym
__timestampnie utworzy sesji, - Ustaw krótki timeout i nie przewracaj żądania na błędzie. Analityka nie jest funkcją krytyczną i nie ma prawa wywalić checkoutu,
- Grupy nie są dopisywane automatycznie. Dokumentacja mówi to wprost: nawet po
assign_grouppolegroupstrzeba przekazywać jawnie przy każdym wywołaniutrack, w którym dane grupy są potrzebne.
3. Przychody powiązane z ruchem
Tracking przychodów to jedna z ciekawszych funkcji i wymaga jednego triku, który wynika wprost z braku ciasteczek. Skoro OpenPanel identyfikuje odwiedzającego przez deviceId, to żeby powiązać płatność z ruchem, trzeba przechwycić ten identyfikator przed zakończeniem płatności i przenieść go przez system płatności.
Przepływ zalecany w dokumentacji jako najbezpieczniejszy prowadzi przez webhook dostawcy płatności. Front pobiera identyfikator urządzenia i wysyła go razem z żądaniem utworzenia sesji płatności:
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deviceId: op.getDeviceId(), // bez tego nie powiążemy płatności
// ...pozostałe dane koszyka
}),
})Backend zapisuje ten identyfikator w metadanych po stronie dostawcy — u Stripe'a to pole metadata sesji płatności — a gdy przychodzi webhook potwierdzający płatność, odczytuje go i wysyła zdarzenie przychodu przypisane do właściwego urządzenia albo profilu. W Laravelu układa się to naturalnie: kontroler tworzący sesję płatności przekazuje deviceId do metadanych, a nasłuchiwacz webhooka wywołuje serwis z poprzedniego punktu.
Dokumentacja opisuje też trzy pozostałe warianty: z backendu dla użytkownika zidentyfikowanego, z frontu (mniej bezpieczny, bo dane o przychodzie wychodzą z przeglądarki) i całkiem bez powiązania z tożsamością — wtedy mamy sumę przychodu bez informacji, skąd przyszedł. Pierwszy wariant jest jedyny wart wdrożenia, jeśli pytanie brzmi „które źródło ruchu przynosi pieniądze", a nie „ile zarobiliśmy".
4. Czytanie danych do własnego panelu
OpenPanel ma trzy typy klientów API i uprawnienia są rozdzielone sensownie:
write— tylko Track API. To klient tworzony razem z projektem,read— Export API i Insights API,root— wszystko, w tym Manage API do zarządzania projektami i klientami. Ma dostęp na poziomie całej organizacji, więc jego sekret traktujemy jak klucz do produkcji.
Limity: Export i Insights 100 żądań na 10 sekund, Manage 20 na 10 sekund, Track wyższe. Przy przekroczeniu wraca 429 i dokumentacja zaleca wykładnicze wycofanie. To wystarcza, żeby wciągnąć wybrane metryki do panelu w Filamencie i pokazać klientowi jego dane bez dawania mu dostępu do całego OpenPanela — co, uprzedzając rozdział o licencji, jest też bezpieczniejsze prawnie.
Serwer MCP, czyli pytanie o dane zamiast klikania w panel
OpenPanel wystawia serwer Model Context Protocol pod https://api.openpanel.dev/mcp, więc analitykę da się odpytywać z Claude'a, Cursora czy innego klienta MCP — bez instalowania czegokolwiek, bo serwer jest hostowany.
Uwierzytelnianie to token będący zakodowanym base64 połączeniem identyfikatora i sekretu klienta:
echo -n "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" | base64claude mcp add --transport http openpanel https://api.openpanel.dev/mcp \
--header "Authorization: Bearer TWOJ_TOKEN"Token można też podać jako parametr zapytania w adresie URL i dokumentacja od razu ostrzega, dlaczego lepiej tego nie robić: adresy trafiają do historii przeglądarki, historii powłoki i logów proxy. Dobre ostrzeżenie w miejscu, w którym większość projektów milczy.
Do MCP wpuszczane są tylko klienty typu read i root; klient zapisujący jest odrzucany. Różnica w zakresie: read jest ograniczony do jednego projektu, root widzi wszystkie projekty w organizacji i może modyfikować dashboardy. To jest właściwy moment na decyzję — asystent z tokenem root potrafi tworzyć, nadpisywać i usuwać raporty oraz dashboardy.
Zestaw narzędzi jest podzielony sensownie i pokrywa większość tego, co robi się w panelu ręcznie:
- Rozpoznanie —
list_event_names(pięćdziesiąt najczęstszych nazw zdarzeń),list_event_properties,get_event_property_values. Dokumentacja radzi zacząć od pierwszego, jeśli nie znamy dokładnych nazw zdarzeń, i jest to dobra rada także dla człowieka, - Dane surowe —
query_eventsiquery_sessionsz filtrami; sesje zwracają czas trwania, stronę wejścia i wyjścia, status odrzucenia i dane atrybucji, - Profile —
find_profiles(szukanie po nazwie, mailu, lokalizacji, bezczynności, liczbie sesji albo wykonaniu konkretnego zdarzenia),get_profile,get_profile_sessionsiget_profile_metricsz metrykami dożywotnimi, w tym przychodem, - Grupy dla B2B —
list_group_types,find_groups,get_groupz właściwościami i członkami grupy, - Metryki zagregowane — od
get_analytics_overviewiget_rolling_active_users(DAU, WAU, MAU) przezget_top_pages,get_page_performanceiget_page_conversionspo podziały na kraje, urządzenia i źródła ruchu, - Zarządzanie dashboardami (tylko
root) — tworzenie, aktualizowanie i usuwanie dashboardów oraz raportów, wraz z ustawianiem pozycji w siatce.
Jest też get_dashboard_urls, zwracające klikalne linki do panelu — drobiazg, który sprawia, że odpowiedź asystenta da się od razu zweryfikować w interfejsie. Dla zespołu, który i tak pracuje z asystentem w terminalu, to realna zmiana w tym, jak często ktokolwiek zagląda w analitykę.
Pułapki
- Trzy systemy stanowe do utrzymania — Postgres, ClickHouse i Redis, każdy z własnym backupem i własnym trybem awarii. To główny koszt self-hostingu i nie widać go z listy funkcji,
- Brak wydań na GitHubie. Wersjonowanie przez gałąź
self-hostingi obrazy Dockera, lista zmian na osobnej stronie dokumentacji. Aktualizacja bez przeczytania tej strony to loteria, - Najnowsze obrazy za 20 USD miesięcznie albo własny pipeline budujący z repozytorium. Publiczne obrazy są opóźnione względem rozwoju,
- Sól rotuje co dobę, więc analityka anonimowa dłuższa niż jeden dzień jest technicznie niemożliwa. Retencja i kohorty wymagają
identify(), - Zdarzenia serwerowe nie tworzą sesji, a zdarzenia starsze niż 15 minut też nie. Łatwo tym zbudować dane, które wyglądają poprawnie i wprowadzają w błąd,
- Bez
notify-keyspace-eventsna zarządzanym Redisie licznik aktywnych odwiedzających pokazuje zero, a wszystko inne działa, - Brak konfiguracji poczty = maile do konsoli. Bez błędu, bez ostrzeżenia w interfejsie,
- Projekt jest młody — pierwszy commit z lutego 2024, 47 otwartych zgłoszeń, jeden główny autor. Przy narzędziu, które ma zbierać dane produktowe przez najbliższe trzy lata, warto mieć plan eksportu (Export API istnieje) i świadomość ryzyka,
- Nie ma SDK dla PHP. Integracja z Laravelem to własny, cienki serwis nad HTTP API — kilkanaście linii, ale nasze kilkanaście linii do utrzymania.
Podsumowanie
OpenPanel jest sensowną odpowiedzią na realny problem: analityka produktowa bez oddawania danych i bez cennika rosnącego z ruchem. Nie jest to jednak narzędzie „postaw i zapomnij" — to platforma z trzema bazami pod spodem, prowadzona jak produkt SaaS, który dodatkowo można postawić u siebie. Co z tego wynika przy planowaniu wdrożenia:
- Projekt jest aktywny, ale młody — 6904 gwiazdki, 67 commitów w miesiącu, pierwszy commit z lutego 2024, 47 otwartych zgłoszeń,
- Instalacja jest naprawdę dopracowana — gałąź
self-hosting,./setupz kwestionariuszem,./start, plus przewodniki dla Coolify, Dokploy i Kubernetesa, - Policz koszt utrzymania trzech usług stanowych przed decyzją o własnej instancji. Przy małym ruchu wersja hostowana bywa tańsza w rzeczywistych pieniądzach,
- Zadbaj o adres
apiUrl, pocztę inotify-keyspace-events— trzy najczęstsze ciche awarie przy self-hostingu, - Rozważ 20 USD miesięcznie za najnowsze obrazy albo zaplanuj własny build. Publiczne obrazy są opóźnione,
- Zrozum mechanizm identyfikacji przed obiecaniem raportów. Sól rotowana co dobę oznacza, że retencja i kohorty wymagają jawnego
identify(), - Nagrania sesji mają dobre domyślne ustawienia prywatności — maskowanie całego tekstu i wszystkich pól, w przeglądarce, przed wysłaniem. Zostaw je włączone i odmaskowuj wybiórczo przez
unmaskTextSelector, - Do warstwy zgody użyj
disabled: trueiready()— rekorder startuje od razu, dane wychodzą dopiero po akceptacji, więc nagranie jest pełne, a zgoda respektowana, - Postaw proxy na własnej domenie, bo inaczej część ruchu nie zostanie zmierzona przez adblockery. W Laravelu to dwie trasy i poprawnie skonfigurowane
TrustProxies, - Powiązanie przychodu z ruchem wymaga
op.getDeviceId()przed utworzeniem sesji płatności i przeniesienia tego identyfikatora przez metadane dostawcy do webhooka, - Serwer MCP wpuszcza tylko klienty
readiroot— arootpotrafi usuwać dashboardy i raporty, więc do odpytywania danych daj asystentowi tokenread, - Z backendu trackuj tylko to, co nie potrzebuje sesji, z krótkim timeoutem i bez przewracania żądania na błędzie analityki.
Licencja: OpenPanel jest rozpowszechniany na GNU Affero General Public License v3.0 — pełnoprawnej licencji open source z najsilniejszym copyleftem w powszechnym użyciu. Kluczowa jest sekcja 13 AGPL: udostępnianie zmodyfikowanej wersji programu użytkownikom przez sieć rodzi obowiązek udostępnienia im kodu źródłowego tych zmian. Przełożenie na typowe scenariusze wygląda tak. Własna instancja obsługująca analitykę naszych aplikacji, także instancja postawiona klientowi na jego infrastrukturze, mieści się w licencji bez zastrzeżeń — o ile nie modyfikujemy kodu, nie ma nawet czego udostępniać. Osadzenie skryptu op1.js na naszej stronie nie czyni z tej strony dzieła pochodnego; strona jest klientem API, nie zmodyfikowaną wersją OpenPanela. Natomiast udostępnienie klientom dostępu do panelu OpenPanela jako części naszej usługi, zwłaszcza po naszych zmianach w kodzie, jest już udostępnianiem programu przez sieć i uruchamia obowiązek z sekcji 13 — trzeba wtedy dać tym użytkownikom kod, razem z modyfikacjami, na AGPL. To ten sam reżim, który omawialiśmy przy MinIO, i ta sama praktyczna konkluzja: jeśli model biznesowy zbliża się do „sprzedajemy klientom dostęp do panelu", pytanie do prawnika albo do autorów projektu jest tańsze niż każda inna droga. Bezpieczna architektura w takim wypadku prowadzi przez Export i Insights API — dane klienta pokazujemy w naszym własnym interfejsie, a panel OpenPanela zostaje wewnętrznym narzędziem zespołu.