Automatyzacja procesów: eliminacja ręcznego wprowadzania danych za pomocą rozszerzenia przeglądarki.

Dodany w dniu 07/01/2022 przez Michał Rostocki

W tym artykule przeanalizujemy podejście, w którym niezbędna jest interakcja człowieka oraz jak można zminimalizować ten wysiłek, aby zoptymalizować powtarzalne zadania i wprowadzić automatyzację procesów biznesowych.

Współczesny świat wymaga od organizacji ściślejszej współpracy. Łańcuchy dostaw muszą być bardziej wydajne, a to wymaga szybkiego i niezawodnego przepływu informacji i danych. Organizacje oczekują od swoich partnerów natychmiastowej odpowiedzi - tak jakby były jedną firmą. Może to być trudne, gdy organizacje używają różnych systemów komputerowych.

Typowe podejście do integracji takich systemów polega na wykorzystaniu istniejącego API. W tym idealnym świecie, jeden system eksponuje API (interfejs programowania aplikacji), a drugi go konsumuje. System konsumujący API będzie również posiadał pewnego rodzaju adapter, który konwertuje pomiędzy systemowymi modelami danych. Po zakończeniu integracji, systemy mogą "rozmawiać ze sobą", ograniczając ludzką interakcję do wysokopoziomowego nadzoru lub konserwacji. Istnieje wiele sposobów na integrację systemów poprzez REST lub SOAP API, bazę danych, wymianę plików na serwerze FTP, a nawet poprzez e-mail.

W tym artykule przeanalizujemy jednak podejście, w którym niezbędna jest interakcja człowieka oraz to, jak można zminimalizować ten wysiłek, aby zoptymalizować powtarzalne zadania i wprowadzić automatyzację procesów biznesowych.

Praktyczny przypadek użycia

Ponieważ wstęp może wydawać się nieco ogólny, przedstawimy praktyczny przypadek użycia. Nasza firma - 3e - oprócz tworzenia oprogramowania na zamówienie, oferuje również usługi wsparcia oparte o umowy SLA (Service Level Agreements). Jednym z najczęściej spotykanych parametrów w SLA jest czas potrzebny na zgłoszenie problemu oraz czas na dostarczenie rozwiązania. Proces ten może trwać od minut w przypadku spraw krytycznych/pilnych do godzin lub dni w przypadku zgłoszeń rutynowych/o niskim priorytecie. W 3e używamy narzędzia Redmine do organizowania zarówno projektów rozwojowych, jak i wsparcia technicznego. Jednak klienci korporacyjni często niechętnie korzystają z systemu dostawcy i pozostają przy swoim dotychczasowym rozwiązaniu.

To jest przypadek, z którym mieliśmy do czynienia w przypadku jednego z naszych większych klientów, który wymagał od nas korzystania z ich wewnętrznego i niestandardowego systemu Ticketing System (STS).

Naszą pierwszą myślą było zintegrowanie Redmine z STS poprzez API. Niestety, nie udało się tego osiągnąć z różnych powodów (STS nie udostępnia API oraz konieczność wzmocnienia bezpieczeństwa). Aby utrzymać to wsparcie, musieliśmy stworzyć zespół wsparcia, który ręcznie aktualizował statusy spraw w obu systemach. Wymagało to jednak czasu i wysiłku oraz powodowało rzeczywiste kilkuminutowe opóźnienia w potwierdzeniu i rozwiązaniu każdego zgłoszenia. Setki, a nawet tysiące zgłoszeń rocznie miałyby znaczący wpływ zarówno na koszty pracy, jak i na jakość usług. Jednak jako firma programistyczna nie poddaliśmy się, lecz szukaliśmy alternatyw dla integracji systemów.

Wymagania

Pierwszym krokiem było zdiagnozowanie najbardziej czasochłonnych zadań i sformułowanie wymagań. W wyniku analizy uzyskano następujące wyniki:

  • najbardziej czasochłonne i prozaiczne zadania wymagają ręcznej synchronizacji danych
  • synchronizowane dane zawierają komentarze użytkowników/developerów oraz załączniki do plików
  • Bilety STS i Redmine pochodzą z różnych modeli danych, a w szczególności mają różne statusy i kategorie

Aby przyspieszyć proces:

  • każdy nowy ticket w STS powinien być wprowadzany automatycznie lub półautomatycznie do systemu Redmine
  • aktualizacje statusu ticketów STS powinny być synchronizowane z Redmine (i vice versa) bez konieczności ręcznej pracy
  • statusy i kategorie biletów powinny być nadawane automatycznie

Wizja i technologia

Dla uproszczenia rozwiązania założyliśmy, że integracja będzie realizowana tylko od strony front-endu. Każdy istotny, wymagany krok będzie przetwarzany półautomatycznie. Powinien on rozpoczynać się od akcji użytkownika (np. po kliknięciu przycisku) i pozwalać użytkownikowi na weryfikację zsynchronizowanych danych przed ostatecznym wysłaniem. Jest to nadal robotyczna automatyzacja procesów, jednak z udziałem agenta oprogramowania.

Wybraliśmy rozszerzenie Tampermonkey dla przeglądarek opartych na Chromium (kompatybilne z Chrome, a także Edge). Tampermonkey, potomek jeszcze starszej wtyczki Greasemonkey dla Firefoksa, jest wszechstronnym rozszerzeniem, które pozwala użytkownikom tworzyć niestandardowe skrypty JavaScript (znane jako skrypty użytkownika), które są wykonywane, gdy adres URL strony pasuje do danego wzorca. Zazwyczaj skrypty użytkownika mogą automatyzować logowanie, usuwać reklamy, zmieniać zachowanie strony lub jej układ, a nawet w niektórych przypadkach pomijać CAPTCHA. Oba rozszerzenia są dość dojrzałe, stworzone początkowo w 2004 i 2010 roku, i powinny być traktowane jako sprawdzone w boju i niezawodne. Należy jednak pamiętać, że skrypty użytkownika mają dostęp do lokalnej pamięci masowej, układu strony, plików cookie i sesji użytkownika. Ponadto, ważne jest, aby zrozumieć zagrożenia bezpieczeństwa i zainstalować zewnętrzne skrypty tylko po dokładnym ich sprawdzeniu.

Szczegóły dotyczące wdrożenia

Pełna implementacja wymagała 28 kb kodu w trzech skryptach. Ponieważ ten artykuł nie jest szczegółowym tutorialem, skupi się tylko na tym, co najważniejsze. Po zainstalowaniu rozszerzenia czas na stworzenie nowego skryptu - w tym celu należy kliknąć na ikonę rozszerzenia i wybrać odpowiednią opcję.

Demonstracja

Oto krótki screencast z wprowadzonym rozwiązaniem. Zaczynamy od strony STS. Po włączeniu rozszerzenia Tampermonkey, pojawiają się pomarańczowe przyciski. Zazwyczaj rozszerzenie jest włączone cały czas. Kliknięcie przycisku "dodaj do Redmine" powoduje zapisanie danych o incydencie w lokalnym magazynie, otwarcie Redmine w nowej zakładce i wykorzystanie danych do utworzenia nowego zgłoszenia. Cały proces jest optymalizowany za pomocą dwóch kliknięć.

Deklaracja skryptu

Tworzenie nowego skryptu otwiera wbudowany edytor Tampermonkey. Należy zacząć od zdefiniowania parametrów skryptu:

// ==UserScript==

// @name STS2Redmine

// @namespace http://tampermonkey.net/

// @version 0.8

// @opis synchronizacja STS z Redmine

// @match https://sts.clientdomain.com/incident*

// @match https://redmine.3e.pl/projects/clien*/issues/new

// @match https://redmine.3e.pl/issues/*/edit

// @grant GM.setValue

// @grant GM.getValue

// @grant GM_xmlhttpRequest

// @grant GM.xmlHttpRequest

// @run-at document-idle

// ==/UserScript==

Nagłówek skryptu zaczyna się od bloku metadanych z różnymi kluczami i wartościami. Podczas gdy niektóre z nich są oczywiste, takie jak nazwa, wersja czy opis, inne będą wymagały odrobiny wyjaśnienia.

@match

Klucz @match definiuje wzorce dla adresów URL, na których wykonywany jest skrypt. Kluczowe jest zawężenie wzorca i zachowanie szczególnej ostrożności w przypadku skryptów, które uruchamiają się wszędzie (*). Zwróć uwagę, że nasz skrypt korzysta z trzech adresów URL: strony incydentów STS, strony Redmine służącej do dodawania nowych spraw oraz strony Redmine umożliwiającej edycję spraw. Typowo, skrypt powinien działać na jednym adresie URL w jednej domenie. Jednak nasz skrypt będzie przenosił dane pomiędzy systemami, korzystając z lokalnego magazynu. Dlatego też musi on obejmować więcej lokalizacji, ponieważ przechowywanie danych jest ograniczone do pojedynczego skryptu.

@grant

Klucze @grant wyraźnie pozwalają skryptowi na użycie dodatkowych funkcji specjalnego przeznaczenia. W naszym przykładzie używamy GM.setValue/GM.getValue, aby uzyskać dostęp do lokalnego magazynu rozszerzeń. Jest to typowy property bag, w którym obiekty mogą być serializowane za pomocą funkcji JSON.stringify.

Skrypt będzie również wykorzystywał funkcję GM.xmlHttpRequest, która będzie używana do wysyłania niestandardowych żądań do Redmine. Oczywiście, JavaScript posiada natywną funkcję XMLHttpRequest, a jQuery może jeszcze bardziej uprościć jej użycie. Jednakże, natywne żądania podlegają polityce Cross-origin resource sharing (CORS) i nie zawsze przyniosą oczekiwane rezultaty. Dlatego wysłanie żądania poziomu rozszerzenia z wyższymi uprawnieniami i działanie poza polityką CORS jest znacznie bezpieczniejsze.

Konfiguracja podstawowa

Teraz czas na skonfigurowanie obiektów core, modułów oraz podzielenie naszego skryptu na dwie domeny.

/* globals jQuery,uploadAndAttachFiles */

(function() {

  'use strict';

  var $ = jQuery;

  var inc = ''; // an STS incident number

  var issue = ''; // a Redmine issue number

    (function ( $ ) {

      $.fn.scrollTo = function(elem, speed) {

        var $this = jQuery(this);

        var $this_top = $this.offset().top;

        var $this_bottom = $this_top + $this.height();

        var $elem = jQuery(elem);

        var $elem_top = $elem.offset().top;

        var $elem_bottom = $elem_top + $elem.height();

        if ($elem_top > $this_top && $elem_bottom < $this_bottom) { return; }

        var new_scroll_top;

        if ($elem_top < $this_top) {

          new_scroll_top = {scrollTop: $this.scrollTop() - $this_top + $elem_top};

        }

        else {

          new_scroll_top = {scrollTop: $elem_bottom - $this_bottom + $this.scrollTop()};

        }

        $this.animate(new_scroll_top, speed === undefined ? 100 : speed);

        return this;

      };

    }( jQuery ));



    (function ( $ ) { $.fn.filled = function() {

      return this.css("background-color", "#FFDDCC"); };

    }( jQuery ));



  if (window.location.host == "sts.clientdomain.com")

  {

    // ...

    return;

  }

  if (/https:\/\/redmine.3e.pl\/projects\/client.*\/issues\/new/.test(window.location.href))

  {

    // ...

    return;

  }

  if (/https:\/\/redmine.3e.pl\/issues\/\d+\/edit/.test(window.location.href))

  {

    // ...

    return;

  }

})();

W pierwszej linii deklarujemy zmienne globalne, które będą używane w skrypcie. Dzięki temu wyłączymy ostrzeżenia o braku deklaracji. Pierwsza z nich to jQuery, która na szczęście jest dostępna w STS (jednak nie pod typową zmienną $), a druga to funkcja do wgrywania załączników w Redmine. Ponieważ zamierzamy mocno zmieniać i używać elementów DOM, jQuery wydaje się być odpowiednim narzędziem do ułatwienia sobie pracy. Jeśli jQuery nie jest wykorzystywane przez stronę, można użyć kluczy @require do dołączenia dowolnego skryptu.

Następnie deklarowane są dwie niestandardowe funkcje jQuery. Pierwsza z nich będzie przewijała okno przeglądarki do wybranego elementu. Funkcja ta będzie wykorzystywana za każdym razem, gdy skrypt będzie ustawiał wartości dla pól wejściowych. Druga z nich to prosta funkcja zmieniająca kolor tła. Funkcja ta jest wykonywana na każdym zmodyfikowanym elemencie wejściowym, aby wizualnie zaznaczyć zmienione elementy.

Na koniec, najważniejszą częścią jest podzielenie skryptu na trzy osobne części wykonywane na różnych adresach URL. Ponownie, jest to konieczne, ponieważ pojedynczy skrypt będzie miał dostęp do tego samego lokalnego magazynu rozszerzeń. Może to wyglądać dziwnie i można by argumentować, że skrypty powinny być dalej podzielone. Jednak lokalny magazyn musiałby zostać zastąpiony przez zewnętrzną usługę lub schowek.

Synchronizacja danych

Synchronizacja danych odbywa się w kilku etapach. Po pierwsze, musimy zebrać wszystkie potrzebne dane. Będzie to wymagało otwarcia Chrome DevTools (lub Developer Toolbar) i znalezienia potrzebnych elementów DOM, a następnie zidentyfikowania ich za pomocą jakiegoś selektora jQuery. Na przykład, aby zidentyfikować problem, nasz skrypt użytkownika powinien zawierać:

inc = $("#sys_displayValue").val();

Na szczęście dla nas, istnieje prosty ukryty input z numerem zdarzenia. Szukanie odpowiednich elementów może zająć trochę czasu, jednak gdy informacja jest już widoczna w przeglądarce, można ją bezpiecznie wyskrobać za pomocą jQuery.

Dalej zbieramy informacje o incydencie i tworzymy link/przycisk, który skopiuje je do Redmine.

// dodaj link do skopiowania sprawy do Redmine

var $ad = $(`<a id='add' class='baton' href=#>add ticket to Redmine</a>`);

$("div.navbar-right span[id^=section_head_right]:first").prepend($ad);

$($ad).click( ()=>{

    // ustawianie wartości

    var ticket = {

        inc : inc,

        des : $("textarea[name='incident.description']").val(),

        imp : $("input[name='sys_display.incident.u_service']").val(),

        trt : $("input[name='incident.u_target_resolution_time']").val(),

        att : jQuery("li.attachment_list_items a.content_editable").get().map(v=>({url:v.href,name:v.innerHTML})),

    }

    GM.setValue("STSincident", JSON.stringify(ticket))

        .then( v=> {

        window.open("https://redmine.3e.pl/projects/client-support/issues/new", '_blank').focus();

    }, reason => {

        console.error("STS2Redmine ::: GM.setVal error ",reason);

    });

});

Tworzony jest prosty div z pojedynczym linkiem zakotwiczającym (id ustawione na `add`). Dołączane jest zdarzenie, które tworzy obiekt ticketu, serializowany jako wewnętrzna wartość STSincident. Właściwości zgłoszenia zawierają numer incydentu, opis, usługę, na którą ma wpływ, czas rozwiązania, a także listę załączników. Lista ta będzie zawierała nazwy i adresy URL tych plików. Na koniec otwieramy okno, w którym zostanie wykonany ten sam skrypt użytkownika, ale w sekcji Redmine, jak wyjaśniono powyżej.

W podobny sposób powinniśmy dodawać linki/przyciski kopiujące komentarze do zdarzeń lub aktualizacje. Ponieważ jednak komentarzy może być wiele, należy utworzyć klasę dla wszystkich linków (selektor a.kdr w przykładzie poniżej).

Dodawanie nowej sprawy

Nie będziemy tutaj opisywać, w jaki sposób dane są wprowadzane w Redmine. Ale podsumowując - otwierany jest nowy formularz sprawy, a gdy skrypt użytkownika odczyta wartość STSincident za pomocą GM.getValue, wszystkie niezbędne pola formularza są wypełniane, a ich kolor tła zmieni się tak, aby zaznaczyć wprowadzone zmiany. Ponownie, założyliśmy, że dana osoba jest już zalogowana w Redmine.

A failsafe - Sprawdzanie czy nie ma już zsynchronizowanych spraw

Na koniec, na wszelki wypadek, możemy sprawdzić, czy sprawa została już przeniesiona do Redmine. W poniższym kodzie nie będziemy korzystać z API Redmine, ale wyślemy proste zapytanie do formularza wyszukiwania i przeskanujemy wszystkie linki w wynikach.

GM.xmlHttpRequest({

    metoda: "GET",

    url: `https://redmine.3e.pl/search?all_words=1&issues=1&titles_only=1&q=${inc}`,

    onerror: ()=>{

        $("a.kdr").remove(); // usuń wszystkie linki kopiujące statusy do Redmine

    },

    onload: function(response) {

        var rt = response.responseText;

        var $ar = $(`a:contains(${inc}):first()`,$(rt));

        if ($ar.size()>0) {

            var href = $ar.prop("href");

            issue = href.split('/').pop(); // (/issues/(\+)/.exec(href)||['',']).pop()

            var status = (/((([\W]\s]+)\)/.exec( $ar.text() )||['','?']).pop();

            // czy chodzi tylko o cyfry

            if (/^\d+$/.test(issue)) {

                $("#add").remove(); // sprawa znaleziona, usuń link dodający ją do Redmine

            }

        }

    }

}).then( ( ()=>{

    if (issue=='') $("a.kdr").remove(); // nie znaleziono sprawy Redmine, usuń linki kopii statusu

});

Gdy żądanie otrzyma odpowiedź, jest ona parsowana. Jeśli sprawa dla danego incydentu została już wprowadzona, usuwamy odpowiedni przycisk, aby uniknąć duplikacji. W przypadku, gdy nie została ona wprowadzona, usuwamy przyciski kopiujące aktualizacje statusu oraz komentarze.

Alternatywy

Oprócz półautomatycznych skryptów front-endowych, można również użyć w pełni zautomatyzowanej przeglądarki. Takie narzędzia są zazwyczaj wykorzystywane do celów testowych, gdzie nagrywa się wiele testów manualnych, a następnie odtwarza je w kontenerowej przeglądarce internetowej. Do najczęściej spotykanych rozwiązań należą:

  • Puppeteer - bezgłowa przeglądarka, która jest zaprogramowana w Nodejs
  • Cypress - framework JavaScript do testowania end-to-end

Hipotetycznie to alternatywne rozwiązanie wymagałoby skonfigurowania przeglądarki internetowej okresowo sprawdzającej dostępność nowych ticketów lub aktualizacji statusu i automatycznie synchronizującej dane między systemami. Może to jednak budzić obawy związane z bezpieczeństwem, ponieważ przeglądarka internetowa musiałaby być uruchamiana przy użyciu rzeczywistych danych uwierzytelniających członka wsparcia.

Podsumowanie

Pokazaliśmy, jak przyspieszyć ręczne wprowadzanie danych i przyspieszyć integrację systemu za pomocą skryptów Tampermonkey. Zapraszamy do kontaktu z nami, jeśli potrzebujesz pomocy w tej dziedzinie.