Jak bylo zmíněno již v prvním článku této série, záměrem Seznamu bylo umístit tlačítko Líbí se nejen na svoje služby, ale také ho nabídnout partnerům. To nám umožňuje získávat lepší zpětnou vazbu od našich uživatelů, které články se jim nejvíc líbí, abychom jim pak mohli lépe nabízet zajímavé články na hlavní stránce Seznamu.
Abychom zajistili co nejlepší rozšíření mezi partnery, museli jsme najít řešení pro integraci do cílové služby s co nejsnazší proveditelností, které by bylo zároveň bezúdržbové. Chtěli jsme zároveň, aby zvolené řešení bylo efektivní i v případě vícenásobného použití na jedné stránce, bylo kompatibilní se single-page webovými aplikacemi a podporovalo vkládání tlačítek do stránky nebo odstraňování existujících tlačítek v libovolný moment. V neposlední řadě jsme chtěli snadné přizpůsobení vzhledu tlačítka – do jisté míry, samozřejmě.
Po technické stránce jsme chtěli zajistit kompatibilitu i se staršími prohlížeči, které podporují pouze ES5. Důvodem k tomu bylo, že v roce 2020 ještě nezanedbatelná část uživatelů používala Windows XP, i když žádný prohlížeč pro tento systém tehdy již nevydával aktualizace nějakou dobu. Také jsme chtěli podporovat (v roce 2020 pro nás ještě relevantní, opět z důvodu používanosti uživateli) Internet Explorer 11 (ten prohlížeč z roku 2013, co měl jako logo modré „e“, pro ty, co to již definitivně vytěsnili z paměti). V dnešní době již podporu pro Internet Explorer 11 nemáme, i když stále máme pro ni všechny nutné předpoklady a jsme schopni ji snadno obnovit.
Návrh architektury a komunikace komponent
S ohledem na výše uvedené požadavky jsme rozdělili frontend tlačítka na
tyto komponenty:
- Integrační skript – skript pro vložení do integrující stránky, který slouží k inicializaci UI jednotlivých tlačítek na označených místech stránky.
- UI – grafické rozhraní samotného tlačítka, včetně logiky pro posílání hlasu uživatele na backend.
- Autentizační fallback – webová aplikace, která slouží pro započítání hlasu pokud selže autentizace uživatele z tlačítka, nebo je nutno uživatele přihlásit a není možno otevřít modální okno s přihlašovacím dialogem. Této části budeme věnovat větší pozornost ke konci článku.
Služba, která by chtěla integrovat tlačítko Líbí se, musí pouze zahrnout do stránky integrační skript a vložit do libovolného vhodného umístění na stránce element(y), do kterého se má tlačítko vykreslit. Toto řešení, kde integrační skript slouží zároveň k inicializaci stavu UI, nám umožňuje taky snadno agregovat do jednoho požadavku na API načtení stavu všech (zatím neinicializovaných) tlačítek na stránce, včetně případné deduplikace ID entit (souhrnné označení pro článek, video, podcast, … nebo jakýkoliv obsah identifikovaný pomocí URL, kterému může uživatel udělit hlas) pokud by bylo tlačítko pro tu samou entitu na stránce víc než jednou. Na hlavní stránce Seznamu jsou desítky tlačítek, takže tato optimalizace nám značně snižuje nároky na hardware.
Interakci jednotlivých komponent a API při spuštění na klientovi znázorňuje následující diagram:
Prohlížeč na začátku (ideálně asynchronně) stáhne integrační skript a spustí ho. Integrační skript vyhledá všechny elementy na stránce, do kterých se má vykreslit tlačítko, a přečte si z každého elementu pro jakou entitu má být dané tlačítko určeno. Po deduplikaci seznamu entit načte integrační skript z API počty hlasů a jak a jestli hlasoval aktuální uživatel pro jednotlivé entity. Pomocí načtených dat jsou následně skriptem inicializovány UI jednotlivých tlačítek.
Pozn.: Seznam entit deduplikujeme také na API, na klientu probíhá deduplikace s cílem zmenšení síťového provozu.
Interakci jednotlivých komponent v reakci na dynamické vkládání nových tlačítek do stránky, například na single-page aplikacích, jsme navrhli následovně:
Podobně jako při inicializaci, i v tomto případě jsme chtěli seskupovat požadavky na načtení stavu tlačítek do jednoho volání API, ovšem museli jsme počítat s postupným objevením existence nových tlačítek po dávkách, jelikož může (třeba kvůli výkonu) samotná aplikace rozdělit velkou změnu UI na několik kroků, nebo i samotný prohlížeč potenciálně může na změny ve stránce upozornit postupně nebo se zpožděním. Z toho důvodu integrační skript při upozornění na nové tlačítko čeká krátkou dobu, jestli neobdrží informaci o dalším novém tlačítku. S každou obdrženou informací se doba čekání restartuje.
Pokud integrační skript neobdrží po nastavenou dobu žádnou informaci o dalším tlačítku k inicializaci, deduplikuje seznam entit nových tlačítek, načte jejich stav a pomocí něj inicializuje UI jednotlivých tlačítek.
Než se ale podíváme na technické detaily, podívejme se spolu z výšky ještě na interakci komponent mezi sebou při aktivaci tlačítka uživatelem:
Uživatel může tlačítko aktivovat například kliknutím myší. Tlačítko na svou aktivaci reaguje podle toho, jestli předpokládá, že je možné uživatele v daný moment autentizovat. Pokud autentizace není možná, automaticky zobrazíme uživateli dialog pro přihlášení (pokud je uživatel autentizován dialogem, dojde k předání této informace a tlačítko pokračuje ve zpracování hlasu uživatele). Pokud předpokládáme, že je alespoň šance, že se podaří uživatele autentizovat, zobrazíme v UI započtení hlasu okamžitě (někteří možná znáte pojem „optimistic update“) a odešleme uživatelův hlas nebo zrušení hlasu na API. Pokud API úspěšně autentizuje uživatele, hlas je započten (pokud již neexistuje) a UI obdrží potvrzení o úspěšném hlasování. Pokud ovšem autentizace uživatele selže, API vrátí UI chybu o neúspěšné autentizaci a UI zobrazí uživateli přihlašovací dialog.
Vraťme se teď ale o něco zpátky a podívejme se spolu do hloubky na technické řešení objevování tlačítek na stránce integračním skriptem.
Objevování tlačítek na stránce
Pro označení elementů stránky, ve kterých se má vykreslit tlačítko Líbí se, existuje vícero způsobů (některé využívané jinými společnostmi), přičemž každý má svá specifická omezení nebo nedostatky:
- Vložení iframe s tlačítkem do stránky – nevýhodou je těžší proveditelnost agregace požadavků na API (i když i to jsme už v minulosti vyřešili u jiného projektu) a spoléhání se na kompatibilní stylování na straně integrující stránky, obzvlášť v případě zpětně nekompatibilních změn na straně UI tlačítka.
- Kontejnerový element (např.
div
nebospan
) inicializovaný kódem stránky za pomoci integračního skriptu – složitější integrace vyžadující vlastní JS kód na straně integrující služby. - Kontejnerový element (např.
div
nebospan
) označený specifickými atributy – složitější a méně výkonné objevování nových tlačítek na stránce, riziko kolize s jinými řešeními v případě nevhodně zvolených identifikátorů. - Sémantický vlastní element – absence nativní podpory ve starších prohlížečích, vedoucí k složitějšímu a méně výkonnému objevování nových tlačítek v těchto prohlížečích, riziko kolize s jinými řešeními v případě nevhodně zvolených identifikátorů, nutnost používat zavírací značku v HTML kódu.
Po zvážení všech pro a proti jsme se rozhodli použít vlastní element (custom element) jako nejflexibilnější řešení s nejlepším výkonem ve valné většině prohlížečů a dostatečně pohodlným použitím, i za cenu nutnosti vyvinutí podpory do nezbytně nutné míry pro starší prohlížeče. Riziko kolizí identifikátorů jsme se rozhodli minimalizovat názvem sestávajícím z názvu společnosti Seznam.cz a z názvu projektu: seznam-pocitadlolibise
.
Použití vlastního elementu se ovšem váže s ještě jednou překážkou způsobenou cílem podporovat i prohlížeče, které podporují pouze ES5 a ne novější verze JavaScriptu (ECMAScriptu): implementace vlastních elementů je reprezentována třídou, ty ale jsou nativní součástí jazyka až od verze ES2015 (označované taky jako ES6). Jelikož třída implementující vlastní element musí dědit od třídy HTMLElement
, a její konstruktor je volán prohlížečem při inicializaci elementu, a ten musí implicitně nebo explicitně (pokud je explicitně uveden v těle třídy) volat konstruktor třídy HTMLElement
, který kontroluje (stejně jako všechny třídy v ES2015) že bylo vytvoření instance vyvoláno s použitím operátoru new
(informace se propaguje při volání super()
v konstruktoru), nevystačili jsme si s překladači (transpilátory) modernějšího JavaScriptu do ES5, protože jimi generovaný kód konstruktorů tříd volá nadřazené konstruktory způsobem, při němž tato nativní kontrola selže.
Taky nebylo řešením volat konstruktor HTMLElement
pomocí operátoru new
v konstruktoru našeho elementu a následně upravit a vrátit vytvořenou instanci – DOM API nepovoluje vytváření elementů tímto způsobem, a to ani když prohlížeč volá váš konstruktor kvůli inicializaci vlastního elementu.
Měli jsme teda před sebou volbu: mohli jsme buďto zvolit jiné řešení než vlastní elementy, nepoužít nativní rozhraní pro podporu vlastních elementů ani v moderních prohlížečích, místo integračního skriptu distribuovat zavaděč, který by stáhnul podle schopností daného prohlížeče ES2015 verzi nebo ES5 verzi integračního skriptu, nebo nalézt nějaké řešení tohoto problému.
Použití nativní podpory vlastních elementů jsme se vzhledem k výkonu vzdát nechtěli, použití zavaděče by také vedlo k snížení výkonu (HTTP2 ani cache prohlížeče toto nekompenzovali dostatečně z našeho pohledu) a jiná řešení jsme zavrhli z důvodů uvedených výše.
Jednou z možností, jak tento problém vyřešit, by bylo použíteval()
nebo jeho ekvivalent
. Jelikož bychom funkci eval()
volali pouze s naším kódem, nepředstavovalo by to bezpečnostní riziko, ovšem zabránilo by nám to v nasazení tlačítka Líbí se na jakoukoliv službu, která zakazuje použití eval()
pomocí Content Security Policy.
Po chvilce bádání jsme narazili na řešení, které stojí na API přidaném spolu s ES2015, ale je kompatibilní s kódem napsaným v ES5: Reflect.construct()
. Tato funkce nám umožňuje v těle konstruktoru vlastního elementu napsaném v ES5 vytvořit instanci HTMLElement
třídy s prototypem naší vlastní třídy, a vrátit tuto instanci prohlížeči jako návratovou hodnotu našeho konstruktoru.
Tím jsme měli vyřešené použití nativního rozhraní pro deklaraci vlastních elementů v moderních prohlížečích, a delegovali jsme tak problém nalézání nových tlačítek Líbí se na stránce na samotný prohlížeč. Zůstávalo vyřešit podporu pro starší prohlížeče bez nativní podpory tohoto API.
Technologicky nejzastaralejší prohlížeč, na jehož podporu jsme cílili, byl Internet Explorer 11, který podporujeMutationObserver
API, pomocí kterého je možno asynchronně sledovat změny v DOM stromu stránky. Toto nám umožnilo vytvořit minimalistické řešení (přibližně 60 řádků kódu) pro obsluhu metod životního cyklu našeho vlastního elementu, které je kompatibilní se všemi prohlížeči, na které jsme cílili.
Vykreslení UI tlačítka
Každé udělátko, které chcete používat na rozličných stránkách mimo vaši kontrolu, čelí problému izolace vlastního vzhledu od nepředvídatelného kódu CSS integrující stránky. Volba vlastního elementu nám ovšem sama nabídla (u podporujících prohlížečů) snadné řešení: shadow DOM.
Zjednodušeně řečeno, shadow DOM je DOM, který reprezentuje vizuální a interaktivní rozhraní daného elementu na stránce, ovšem samotný DOM uvnitř shadow DOM je izolován od DOM a CSS stránky. Navíc je možné shadow DOM znepřístupnit i JavaScriptovému kódu stránky, s výjimkou kódu, jenž jej vytvořil (nebo na něj obdržel referenci od kódu, který jej vytvořil) – jedná se v takovém případě o tzv. uzavřený mód.
Pokud prohlížeč nepodporuje shadow DOM, je možné docílit stejné míry izolace od zbytku stránky (i když za cenu vyššího dopadu na výkon) vykreslením UI v iframe
. Obsah dokumentu uvnitř iframe
je možné nastavit pomocí atributu srcdoc
, nebo uzavřením obsahu do JavaScriptového řetězce a použitím pseudo-protokolu javascript
v src
atributu iframe
elementu (například: javascript:'<p>Ahoj <strong>světe!</strong></p>'
), případně manipulací obsahu vnořeného dokumentu pomocí DOM API pokud je src
atribut iframe
elementu nastaven na URL s about
pseudo-protokolem, např. about:blank
(v takovém případě má nadřazená stránka úplný přístup k obsahu vnořeného dokumentu).
Tento přístup k zobrazení UI má ovšem jeden zásadní nedostatek: požadavky na API budou mít Origin
HTTP hlavičku nastavenou na hodnotu odpovídající integrující službě, takže bychom nebyli schopni rozlišit mezi našimi požadavky na API na započítání hlasů uživatelů a podvodnými požadavky na započítání hlasů pod identitou aktuálního uživatele, které by si integrující služba posílala sama i bez interakce s uživatelem.
Zvolili jsme teda jiné řešení pro UI: v iframe
z naší domény načtený statický HTML soubor, který obsahuje vše potřebné pro zobrazení UI, a pouze obdrží inicializační data od integračního skriptu. Odevzdání inicializačních dat v hash části URL (která není prohlížečem ani posílána na server) statického souboru nám umožňuje využít cache na straně prohlížeče pro zlepšení výkonu inicializace UI. Navíc pomocí cache busting parametru v URL, nastaveného například na číslo aktuálně nasazené verze, můžeme zároveň zajistit, že se nebude používat starší než aktuální verze tohoto souboru. Po zvážení potenciálního rizika vypršení cache tohoto HTML souboru dřív než dojde k vypršení cache integračního skriptu v případě nutnosti nasazení zpětně nekompatibilních úprav jsme zvolili použití hodnoty cache busting parametru odvozené z času s adekvátní granularitou.
Uzavření UI do iframe
na vlastní doméně má ale jednu nevýhodu: UI nedokáže změnit svou šířku (šířku iframe
elementu) samo, a naše UI zobrazuje počet udělených hlasů, takže šířka se může měnit i po inicializaci při udělení nebo odebrání hlasu. Jako řešení jsme museli zvolit kontrolu velikosti prostoru, které UI potřebuje pro správné zobrazení po každé aktualizaci UI. Pokud UI potřebuje více prostoru, pošle zprávu integračnímu skriptu pomocípostMessage()
API a integrační skript provede potřebnou změnu. Integrační skript vždy kontroluje, že zpráva přišla od tlačítka, které spravuje a ze správného původu (originu/domény). Obsah zprávy neobsahuje žádné citlivé údaje, takže není nutné další zabezpečení této komunikace.
Problémy s vykreslením UI v Safari
Zobrazení UI v iframe
přineslo zajímavý nečekaný problém v prohlížeči Safari, způsobený tím, co a jak tento prohlížeč ukládá do cache a jak s těmito informacemi nakládá při procházení historie.
Safari cachuje obsah iframe
elementů, včetně jejich vizuální reprezentace, a tento obsah obnoví při navigaci zpět v historii procházení webu. Naneštěstí tato obnova není dokonalá a docházelo k prohození obsahu mezi vícero tlačítky na stránce (např. na hlavní stránce www.seznam.cz je u každého článku v nekonečném proudu článků tlačítko Líbí se pro daný článek).
Obzvlášť zajímavé na této chybě bylo, že i když bylo uživateli poskytnuto nesprávné UI (i když v plně funkčním stavu), obsah daného dokumentu v iframe
vypadal správně z pohledu JavaScriptového kódu a taky vývojářských nástrojů samotného prohlížeče.
Pozn.: Tuto chybu jsme pozorovali ve verzi Safari, která byla aktuální v době vydání tlačítka Líbí se. Je možné, že se v současné verzi neprojevuje.
Pro obejití tohoto problému jsme nejdřív vykreslili v iframe
stránku s (pseudo)náhodou adresou s about
pseudo-protokolem, aby nedocházelo při inicializaci UI k mapování na nacachovaný obsah. Následně jsme pomocí volání funkcelocation.replace()
(aby nedocházelo k vytváření nových položek v historii prohlížeče pro každé tlačítko) uvnitř vykresleného dokumentu (každá stránka může takhle přesměrovat libovolný svůj iframe
, navíc k dokumentům načteným about
pseudo-protokolem má JavaScriptový kód nadřazené stránky úplný přístup) nahradili tento prázdný dokument skutečným UI tlačítka.
Toto řešení má i jednu výhodu – umožňuje nám v parametrech URL pro UI tlačítka odevzdat i potenciálně citlivé informace, jelikož k nim takhle nemůže integrující stránka přistupovat. Je to způsobeno tím, že src
atribut i vlastnost daného iframe
elementu si zachová původní hodnotu obsahující vygenerovanou náhodnou URL. Kvůli tomu není pro nadřazenou stránku možné přečíst žádné informace z location
objektu vnořené stránky pokud je jiného původu (originu).
Autentizace uživatele napříč internetem
Tlačítko Líbí se vyžaduje určení (anonymizované) identity uživatele ze dvou důvodů: správné započítání hlasu uživatele a zobrazení, jestli již aktuální uživatel hlasoval pro danou entitu.
Historicky, a v počátcích služby Líbí se, se k tomuto účelu daly použít cookies v prohlížeči, které se (občas na explicitní požádání kódu provádějící HTTP požadavek) přenášely i s požadavky na jiné původy (originy/domény). Naneštěstí byl tento mechanismus také nemálo zneužíván různými systémy k sledování uživatele do značné míry napříč stránkami.
Jelikož se, pochopitelně, v společnosti i technologické komunitě čím dál víc ozývaly hlasy volající po lepší ochraně soukromí uživatelů, které vedly také k legislativní snaze zlepšit celkovou situaci, nebylo moc překvapivé, že i samotné prohlížeče začaly situaci řešit a nenechávaly řešení problému pouze na rozšířeních, legislativě a různorodé míře ohleduplnosti ze strany webových aplikací, s cílem poskytnou jednoduší a ideálně i lepší řešení pro všechny své uživatele. (O dokonalosti různých řešení se dá, jako vždy, dlouze debatovat, stejně tak o motivacích a snahách jednotlivých stran, ale to není téma tohoto článku.)
Tyto ochrany byly ze začátku relativně mírné – pokud uživatel relativně nedávno interagoval s tlačítkem Líbí se, cookies se přenášely. Otázkou bylo pouze jak zajistit zobrazení informace, že uživatel hlasoval i po o něco delší době. Tento problém bylo možné řešit na straně klienta ukládáním si nedávných hlasů do lokálního úložiště a uživatelský zážitek tak nebyl narušen.
Situace se ale vyvíjela poměrně rychle a my jsme museli reagovat. Jeden z nejsilnějších posunů vpřed byl ze strany Apple a jejich prohlížeče Safari, obzvlášť od uvedení technologie Intelligent Tracking Prevention 2.0, a jejích různých dalších aktualizací a vylepšení (tyto odkazy nepředstavují vyčerpávájící výčet). Jedná se o opravdu působivé řešení, které nám ale zkomplikovalo naši snahu identifikovat uživatele při snaze zároveň zachovat co nejlepší uživatelský zážitek.
Firefox postupoval trochu odlišnou cestou, a postupně zaváděl čím dám tím plošnější izolaci až blokování cookies 3. stran (cookies, které patří k jiným původům (originům/doménám), než je stránka načtená na nejvyšší úrovni v okně prohlížeče). Izolace cookies (využívaná v minulosti také prohlížečem Safari) spočívá v použití odděleného úložiště pro cookies daného původu (originu) v závislosti na tom, v jakém kontextovém původu (originu) byl dokument 3. strany načten. Příkladem: cookies pro doménu seznam.cz v iframe
na stránce root.cz jsou oddělené od cookies pro doménu seznam.cz v iframe
na stránce seznamzpravy.cz.
Jedno z řešení situace, které je doteď součástí tlačítka Líbí se, je záložní uživatelské rozhraní, které otevíráme v modálním okně, a které má tím pádem plný přístup ke cookies. V něm provádíme pouze autentizaci uživatele (s případným přihlášením v případě potřeby), započteme hlas, oznámíme hlavnímu UI jestli se operace povedla a modální okno zavřeme. Pokud je tedy uživatel přihlášen a vše bude fungovat dle očekávání, tak jediným negativním dopadem pro uživatelský zážitek bude probliknutí modálního okna.
Dále, Firefox a Safari zavedli podporu Storage Access API, které umožňuje kódu 3. strany uvnitř iframe
získat v momentě interakce uživatele s obsahem iframe
přístup k cookies původu (originu), ze kterého je obsah iframe
načten. Podpora tohoto API není konzistentní mezi těmito prohlížeči, ale je použitelná. Nevýhodou ale je, že se uživateli nejdřív zobrazí výzva prohlížeče, jestli chce dovolit stránce v iframe
(uživateli se ve výzvě zobrazí také eTLD+1 doména stránky načtené v iframe
) aby jej/ji mohla sledovat, což naruší uživatelský zážitek (ale je zároveň pochopitelná a užitečná součást ochrany proti nechtěnému sledování. Tato výzva se nezobrazuje opakovaně v případě nedávného přijetí nebo zamítnutí žádosti uživatelem). V případě, že uživatel výzvu odmítne nebo není přihlášen, můžeme použít modální okno popsané výše.
Pro další zlepšení uživatelského zážitku můžeme využít toho, že uživatel může být přihlášen na dané službě Seznam.cz, která tlačítko integruje. V takovém případě můžeme pro přihlášeného uživatele vygenerovat jednorázový token s krátkodobou platností použitelný pouze pro účely sociálních doplňků, jako je tlačítko Líbí se. Tento token pak použijeme při komunikaci integračního skriptu nebo UI s API pro autentizaci uživatele a výše uvedená řešení slouží jako záložní řešení pro případ, že uživatel není přihlášen na integrující službě. Za ideálních podmínek nebude uživatelův zážitek nijak narušen alespoň na službách Seznamu.
Šíření tlačítka českým internetem
Bez ohledu na to, jak skvělý máte produkt nebo službu, nepomůže vám to samo o sobě, pokud ji nedokážete dostat k lidem. Ideálně k co největšímu počtu lidí.
Ačkoliv jsme navrhli tlačítko Líbí se tak, aby bylo možné ho integrovat do libovolné služby co nejsnadněji, ne každá technologie umožňuje tak snadnou integraci takovéhoto řešení za všech okolností, a ne každý partner, kterému bychom chtěli tlačítko nabídnout, má k dispozici dostatečně technicky zdatné lidi, kteří by integraci zajistili.
V Seznam.cz má každý tým volné ruce pokud jde o volbu technologií, na kterých chce „svou“ službu stavět, na frontendu se u nás ale nejčastěji setkáte s použitím Reactu na vykreslení stránky, a v nemálo takových případech stojí frontend služby na frameworku IMA.js.
V případě použití Reactu na službě nastává potenciální problém s integrací tlačítka Líbí se pokud služba vykreslí HTML kód stránky na serveru a následně pomocí Reactu „oživí“ DOM na klientu. Tento problém spočívá v race condition pokud by došlo k inicializaci tlačítka Líbí se před „oživením“ DOM stránky ze strany Reactu. React by v takovém případě odstranil obsah elementu seznam-pocitadlolibise
a tlačítko by nebylo vidět. Pro usnadnění integrace do těchto služeb jsme proto vytvořili React komponentu, která toto chování ošetřuje a zajišťuje správnou inicializaci.
Naši partneři naopak nejčastěji používají buďto WordPress, nebo nějaké vlastní řešení. S vlastními řešeními sice moc nezmůžeme, ale vytvořili jsme plugin pro WordPress, abychom usnadnili integraci co největšímu počtu našich partnerů.
Pro všechny ostatní, kteří by chtěli vytvořit nějaké integrační řešení pro jiná CMS nebo RIA JS knihovnu, jsme nakonec zveřejnili užitečné pomocné kódy v JavaScriptu/TypeScriptu a PHP, na kterých stojí také výše odkazovaná řešení.
V neposlední řadě jsme zveřejnili dokumentaci na Seznam.cz stránce pro vývojáře v sekci „Doplňky pro web,“ včetně odkazů na výše uvedená řešení.
Jak si za tu dobu tlačítko Líbí se vlastně vede? Začátkem roku 2022, kdy jsme prováděli migraci z Couchbase na PostgreSQL, jsme měli v databázi více než 19 milionů hlasů udělených více než 900 tisícům entit.
Ptáte se jaká migrace z Couchbase na PostgreSQL? Proč, jak, a co nám to přineslo? Inu, o tom zase někdy příště.