[ kalizi.dev ]

Strategie Data-Driven con Laravel #2: configura la registrazione delle sessioni utente

Photo by Kushagra Kevat on Unsplash

“No matter how cool your interface is, it would be better if there were less of it.” ~ Alan Cooper

La prima volta che ho letto questa citazione l'ho trovata piuttosto divertente, perché ogni volta che ragioni con il tuo UX Designer sul legame tra interfaccia e funzionalità serve tantissimo lavoro per quel pulsante, o per quel colore del testo, o per quell'ombra… insomma, se ci hai mai lavorato, sai bene com'è!

Il vero problema è che mentre progetti una UX devi concentrarti sui comportamenti degli utenti e… sii realista, tu non sei i tuoi utenti e nemmeno il tuo UX Designer lo è! Sei di parte, questo è un dato di fatto, e devi accettarlo.

Ovviamente non sto dicendo che il tuo UX Designer non sia bravo nel suo lavoro: il processo di UX Design è qualcosa di davvero difficile e richiede molta creatività, ricerca e concentrazione, ma di tanto in tanto può aver bisogno di un aiuto dall'esterno.

E l'esterno di cui parlo è fatto dei tuoi utenti! Come puoi includere i tuoi utenti nel design della UX? È piuttosto semplice: registrandoli.

Può suonare strano se non ci hai mai pensato. Ora immagino che tu stia pensando a cosa succede quando entri in un sito, muovi il mouse e qualcuno dall'altra parte ti sta guardando in pigiama mentre scorri il feed per le ultime notizie, che imbarazzo! Quando parlo di registrare gli utenti mi riferisco alle sessioni utente: l'intero arco temporale, i movimenti del mouse e gli eventi che accadono da quando l'utente apre la prima pagina fino a quando la chiude. È interessante vedere come un utente si comporta davvero sul tuo sito, perché può portarti a riconsiderare molte cose. Per esempio, può mostrare che gli utenti non usano alcune funzionalità perché non le trovano facilmente accessibili, oppure che una funzionalità non viene usata come pensavi.

Registrare le sessioni utente può davvero cambiare il tuo modo di progettare una UX, quindi immergiamoci e vediamo come funziona!

Setup

Molti servizi online ti permettono di registrare le sessioni utente, alcuni sono gratuiti come Yandex Metrika, altri a pagamento come Hotjar, dipende tutto da cosa vuoi ottenere. Qui proveremo a costruire la nostra soluzione personalizzata per la registrazione dello schermo basata su Cimice, un session recorder sperimentale che puoi trovare su Github.

Tutto ciò di cui hai bisogno per iniziare a registrare le sessioni è la build di cimice disponibile in dist/cimice.min.js.

Per aiutarti a registrare le sessioni utente su ogni pagina basta creare un file blade personalizzato da includere dove serve. Il file views/cimice_recording.blade.php conterrà:

<script src="{!! asset('public/path/to/cimice.min.js') !!}"></script>

Questo è solo l'import dello script cimice, ma va anche configurato. Il secondo esempio su Github è davvero semplice ma per scopi di test o casi molto semplici, semplicemente funziona.

<script>
    function sendRecordings(recordings) {
        let movieJson = JSON.stringify(recordings);
        // Your custom code to send movieJson
    }
</script>
<script src="{!! asset('js/cimice.min.js') !!}"></script>
<script>
    let rec = new cimice.Recorder({
        target: document.documentElement
    });

    // At first, when the recording starts I want to be sure to send initial
    // data about the movie/target/screen
    rec.on('startRecording', () => {
        sendRecordings(rec.getMovie());
    });

    // Next listener sends last new recorded frames every 50 interactions (with default mousemove
    // event it's already pretty much high frequency) and remove them from the collection.
    // Anyway this logic is pretty much simple but ok as example, I suggest to build your own.
    rec.on('recording', () => {
        let movie = rec.getMovie();
        let frames = movie.getFrames();
        if(!(frames.length % 50)){
            sendRecordings(frames);
            movie.setFrames([]);
        }
    });

    rec.startRecording();
</script>

View on GitHub Gist

Se metti un console.log(movieJson) per controllare cosa c'è dentro, vedrai un oggetto JSON contenente:

  • frames: un array che contiene i frame registrati.
  • left: offset sinistro della pagina.
  • top: offset superiore della pagina.
  • scrollX: offset di scroll orizzontale.
  • scrollY: offset di scroll verticale.
  • scene: l'HTML attualmente su schermo, codificato in Base64.

Nota che se l'HTML varia durante la stessa sessione di pagina, dovresti modificare lo script qui sopra per passare l'intero movie invece di inviare solo i frames.

Se vuoi salvare l'intera sessione nel tuo database, devi usare un model appropriato tramite la relativa migration php artisan make:migration create_session_recordings_table --create=session_recordings. La tabella dovrebbe conservare:

  • session_id: la sessione registrata.
  • user_id: l'utente che stai registrando (se vuoi tracciarlo).
  • payload: il JSON inviato dal client.
  • previous_session_recordings_id: verrà usato per concatenare ogni registrazione inviata dal client.
$table->id();
$table->string('session_id');
$table->foreignId('user_id')
      ->nullable()
      ->constrained('users')
      ->onUpdate('cascade')
      ->onDelete('cascade');
$table->json('payload');
$table->timestamps();

Vogliamo usare questi dati tramite un Model, quindi basta php artisan make:model SessionRecording. E vogliamo anche assicurarci che ogni proprietà sia fillable.

Ora dobbiamo preparare un'API per salvare le nostre registrazioni, iniziamo con un controller php artisan make:controller SessionRecording\RecordingController e una request php artisan make:request SessionRecording\StoreRecordingRequest.

Per mantenere la request semplice vogliamo autorizzarla solo quando arriva una richiesta JSON e mettere i nostri dati nelle rules.

public function authorize()
{
    return $this->wantsJson();
}
public function rules()
{
    return [
        'frames' => 'required_if:scene,=,null|array',
        'left' => 'sometimes|nullable|numeric',
        'top' => 'sometimes|nullable|numeric',
        'scrollX' => 'sometimes|nullable|numeric',
        'scrollY' => 'sometimes|nullable|numeric',
        'scene' => 'sometimes|string',
    ];
}

Anche la logica del controller sarà semplice, deve solo salvare i dati.

public function store(StoreRecordingRequest $request)
{
    $inputs = $request->only(['frames','left','top','scrollX','scrollY','scene']);
    $user = $request->user();
    SessionRecording::create([
        'session_id' => $request->session()->getId(),
        'user_id' => $user ? $user->id : null,
        'payload' => $inputs
    ]);
    return response()->json([
        'success' => true,
    ]);
}

Così la sendRecordings può essere finalmente modificata:

function sendRecordings(recordings) {
    let movieJson = JSON.stringify(recordings);
    fetch(
        '{!! route('recordings.store') !!}',
        {
            method: 'POST',
            body: movieJson,
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-CSRF-Token': '{!! csrf_token() !!}'
            },
        }
    ).then(console.log);
}

Tieni presente che questo è solo codice di esempio, puoi adattarlo al tuo uso.

La registrazione

Photo by Fabrizio Verrecchia on Unsplash

Registrare le sessioni utente ora è semplicissimo, devi solo includere il file blade di cimice, ed è fatta!

@include('cimice_recording')

Tutto qui, niente altro da fare!

Lo sviluppatore attraverso lo specchio

Una volta che tutto è configurato e hai iniziato a registrare le sessioni, probabilmente vorrai anche vedere cosa è stato registrato.

Tieni presente che qui non costruiremo una dashboard per guardare le sessioni e proteggere i dati dall'essere visti da utenti non autorizzati, quello sta a te. Se vuoi costruire velocemente una dashboard, puoi dare un'occhiata al mio altro articolo su Infyom Generator🤓!

Qui vogliamo raccogliere i dati dal nostro database. La struttura del database che ho delineato prima è davvero semplice, puoi raggruppare i dati per session ID o per user ID, oppure puoi aggiungere altri dati alla struttura del database per aiutarti a raccogliere dati più specifici: per esempio, puoi aggiungere lo user-agent, gli header della request, oppure aggregare questi dati con altri sistemi di analytics, raccoglierli sta a te!

Per recuperare ogni sessione registrata in questo modo puoi semplicemente:

SessionRecording::distinct('session_id')->get('session_id');

Oppure, se vuoi ogni ID relativo a una sessione usando la sintassi raw di MySQL/MariaDB, puoi ottenerli così:

SessionRecording::groupBy('session_id')->selectRaw('session_id, GROUP_CONCAT(id) AS ids')->get();

Se le sessioni registrate sono troppo frammentate puoi anche unirle usando reduce in questo modo:

$recordings = SessionRecording::where('session_id', '=', 'a_user_session_id')
    ->orderBy('created_at')
    ->get()
    ->reduce(
        function ($rec, $v) {
            if (empty($rec)) {
                $rec = json_decode($v->payload, true);
            } else {
                $rec['frames'] = array_merge($rec['frames'], json_decode($v->payload, true)['frames']);
            }
            return $rec;
        },
        []
    );
usort($recordings['frames'], function ($v1, $v2) { return $v1['ts'] - $v2['ts']; });

Come funziona?

  • Ottieni ogni registrazione relativa a una sessione specifica tramite la query
  • Il reducer parte usando un array vuoto
  • La prima volta il reducer mette semplicemente il payload dentro l'array ridotto
  • Da lì in poi, ogni volta i nuovi frame registrati vengono uniti ai frame precedenti.
  • Dopodiché, giusto per stare sul sicuro, la usort ordina i frame in base alla proprietà ts (timestamp).

Ora che hai tutte le tue registrazioni, sei pronto a mostrarle! Puoi mettere il codice per mostrare la sessione registrata in un file separato come cimice_play_script.blade.php.

<script src="{!! asset('js/cimice.min.js') !!}"></script>
<script>
function playMovie(jsonMovie, player) {
      let movie = new cimice.Movie(jsonMovie);
      let player = new cimice.Player({
          target: player
      });
      player.on('mousemove', function(frame){
          let dot = document.createElement("div");
          dot.style.backgroundColor = 'blue';
          dot.style.width = '10px';
          dot.style.height = '10px';
          dot.style.borderRadius = '100%';
          dot.style.marginLeft = '-5px';
          dot.style.marginTop = '-5px';
          dot.style.position = 'absolute';
          dot.style.left = player.getCursorX() + 'px';
          dot.style.top = player.getCursorY() + 'px';
          player.getTarget().appendChild(dot);
      });
      player.on('click', function(frame){
          let dot = document.createElement("div");
          dot.style.backgroundColor = 'red';
          dot.style.width = '10px';
          dot.style.height = '10px';
          dot.style.borderRadius = '100%';
          dot.style.marginLeft = '-5px';
          dot.style.marginTop = '-5px';
          dot.style.position = 'absolute';
          dot.style.left = player.getCursorX() + 'px';
          dot.style.top = player.getCursorY() + 'px';
          player.getTarget().appendChild(dot);
      });

      player.setMovie(movie);

      player.play()
}
</script>

View on GitHub Gist

Nota che puoi modificare come il player mostrerà ogni singolo evento. Gli eventi registrati possono essere mousemove, click, scroll, resize, contextmenu (puoi anche metterti in ascolto di qualsiasi altro evento, ma devi modificare la configurazione iniziale), quindi usando player.on('event', ...) puoi modificare come apparirà il tuo mouse.

Una volta messo questo file nella tua cartella views, dovresti semplicemente fare qualcosa del genere:

<div id="player"></div>
@include('cimice_play_script')
<script>
    let movieJSON = {!! json_encode($recordings) !!};
    playMovie(movieJSON, document.getElementById('player'));
</script>

Questo creerà un iframe nel div passato come argomento che mostrerà la sessione utente!

Esempio di sessione utente come GIF

Ricorda che una sessione utente non è necessariamente lineare: ogni utente può passare a un'altra finestra, aspettare qualche secondo senza fare niente, aprire la scheda degli sviluppatori, non puoi prevedere con esattezza come l'utente userà il tuo sito, ma è un inizio!

Ma… la privacy?

Lo scopo delle tue Data-Driven Strategies dovrebbe sempre essere quello di offrire agli utenti un servizio migliore!

I tuoi utenti non sono cose da sfruttare per i tuoi scopi. Se stai costruendo qualcosa del genere è perché vuoi migliorare il tuo servizio, renderlo più usabile, dare ai tuoi utenti un'esperienza migliore. I tuoi KPI e le tue metriche esistono grazie ai tuoi utenti, e se vuoi costruire qualcosa del genere dovrebbe essere perché vuoi dare loro un servizio che possano usare e che apprezzino.

I tuoi utenti dovrebbero essere sempre consapevoli di come raccogli e usi i loro dati: ricordati di esplicitare questo tipo di sistema nella tua privacy policy, anonimizza i dati dove non hai bisogno che siano collegati e mantieni e rispetta una data retention policy in cui sia esplicito quando i dati verranno cancellati.

Ora sta a te costruire qualcosa di grandioso! 🥂

Resta sintonizzato per altre Data-Driven Strategies e se vuoi, prenditi un momento per lasciare un commento su come prendi decisioni data-driven supportate dagli strumenti! ☕️