Neuroróżnorodność w miejscu pracy
16.11.202311 min
Przemysław Jastrowicz

Przemysław JastrowiczEngineering Manager

Audyt technologiczny - po co i jak go zrobić

Dowiedz się, co oznacza audyt technologiczny, po co go robić oraz jak może pomóc zaoszczędzić czas, siły i pieniądze.

Audyt technologiczny - po co i jak go zrobić

Audyt technologiczny w ogólnym rozumieniu jest bardzo podobny do każdego innego audytu, na przykład finansowego. Jego zadaniem jest dokładne zbadanie i zdiagnozowanie Twojej aplikacji, abyś w przyszłości mógł podejmować lepsze decyzje oparte na danych, a nie na przekonaniach i przypuszczeniach. Skupię się tutaj na aplikacji frontendowej napisanej w Angularze, ale zawarte tutaj porady i praktyki możesz śmiało zastosować do aplikacji napisanych w dowolnym frameworku.

Wykonanie audytu jest rewelacyjnym sposobem na głębsze poznanie swojej aplikacji. Nawet wtedy, gdy na pierwszy rzut oka nie wykazuje ona żadnych wad i niedoskonałości, a użytkownicy nie skarżą się na jej jakość. Twój zespół będzie mógł zareagować zanim takie problemy wystąpią - zgodnie z zasadą, że lepiej zapobiegać niż leczyć. Może się również zdarzyć, że odkryjesz wady swojej aplikacji, o których dotychczas nie wiedziałeś.

Przejdę zatem do kroków niezbędnych do przeprowadzenia audytu technologicznego.

1. Zdefiniuj swojego użytkownika

To jest kluczowy krok, ponieważ to właśnie on definiuje wszystkie kolejne działania. Musisz przede wszystkim zastanowić się, kim dokładnie jest Twój użytkownik. Najczęściej okazuje się, że masz więcej niż jeden typ użytkowników. Na przykład, aplikacja, którą audytowałem, była skierowana do trzech głównych grup użytkowników: nowych potencjalnych użytkowników (landing page, strony FAQ, o nas, itp.), już pozyskanych użytkowników (strony z potwierdzeniem zamówienia, strony płatności, faktyczna treść aplikacji, itp.) oraz wewnętrznych użytkowników (szeroko pojęty panel admina).

2. Zdefiniuj, czego oczekuje Twój użytkownik

Gdy już wiesz, kim są ludzie, których obsługuje Twoja aplikacja, musisz zadać sobie pytanie, czego tak naprawdę od niej oczekują i co jest dla nich najważniejsze. Np. z perspektywy marketingowej dla potencjalnego klienta jednym z najważniejszych czynników jest wydajność aplikacji. Strona musi ładować się szybko i prezentować się pięknie pod względem interfejsu.

Dla już pozyskanego klienta najważniejsza jest czytelność i łatwość korzystania z narzędzia, podczas gdy dla pracownika Twojej firmy liczy się przede wszystkim stabilność i niezawodność systemu. To nie oznacza, że wymienione aspekty nie są ważne dla innych grup użytkowników, ale dla admina Twojej strony ważniejsze jest to, czy jego działania w 100% odzwierciedlają to, w jaki sposób zarządza stroną, niż to, czy układ panelu admina jest przejrzysty, czytelny i intuicyjny od pierwszej chwili.

3. Wybierz metryki które są dla Ciebie istotne

Gdy już wiesz, co i komu serwujesz, zastanów się nad tym, jak opisać to wszystko za pomocą liczb. Dzięki temu będziesz mógł łatwo monitorować, czy to, co robisz, rzeczywiście wpływa na wskaźniki, które są dla Ciebie ważne. Oto kilka przykładów metryk, które wybraliśmy dla naszej aplikacji:

Web Vitals: Wszystkie metryki zawarte w Web Vitals są szczególnie istotne dla potencjalnych użytkowników. Nie będę opisywać różnicy między FCP a LCP itp. Chciałbym tylko zwrócić uwagę, że pomimo iż są to bardzo podstawowe metryki, wciąż są developerzy (a tym bardziej PMowie), którzy o nich zapominają. Metryki te są banalnie łatwe do zmierzenia i śledzenia, więc stanowią niejako punkt wyjściowy do całej reszty. Ważna uwaga: niezależnie od tego, z jakiego narzędzia korzystasz do pomiaru tych metryk, nie rzucaj się na ślepo w dążeniu do osiągnięcia 100% w każdej kategorii. Czasami jest to po prostu niemożliwe. Nie zapominaj, że narzędzia takie jak Google Lighthouse nie są idealne i nigdy nie powinny być wyrocznią.

Bundle size - kolejna metryka która jest dosyć oczywista ale w dobie ultra szybkiego internetu i mocnych maszyn zapominamy, że każdy kolejny kB w naszej początkowej paczce to kolejne milisekundy potrzebne do  pobrania, przecztania i wykonania jego zawartości. Warto sprawdzać jak rośnie (lub maleje) nasza początkowa paczka gdyż ma ona ogromne znaczenie dla całej naszej aplikacji. Z pomocą przychodzą nam tutaj narzędzia takie jak webpack-bundle-analyzer dzięki któremu nie tylko łatwo zmierzymy rozmiar naszej paczki ale także zobaczymy co wchodzi w jej skład

Pokrycie testami - znowu, sprawa wydaje się oczywista natomiast wcale tak nie jest. Spotkałem wiele projektów które miały bardzo niskie lub nawet zerowe pokrycie testami. Jakość i liczba naszych testów upewni nas, że nasza aplikacja faktycznie robi to co powinna. Pomocna może się tu okazać piramida testów która zakłada że podstawą powinny być testy jednostkowe, których powinno być najwięcej,  a im wyżej piramidy tym mniej testów e2e oraz testów integracyjnych. Oczywiśćie sama ilość to nie wszystko dużo lepszy jest jeden dobry test niż dziesięć kiepskich, zwłaszcza jeśli chodzi o aplikacje frontendowe w którcyh pisanie testów nie należy do najłatwiejszych i najprzyjemniejszych.

Developer Experience (DX):  DX to szeroki temat który można rozważać na wielu płaszczyznach. Najczęściej jednak szczęśliwy developer to efektywny developer. Warto sprawdzić jak programiśći w naszym zespole czują się pracując z aplikacją. Może są aspekty które nie wpłyną w żaden sposób na funkncjonowanie aplikacji a usprawnią życie wielu osobom. Polecam przeprowadzić krótką ankietę wśród zespołów które rozwijają aplikację. Może okazać się, że mamy aplikacje która funkcjonuje nienajgorzej, natomiast nikt w firmie nie chce jej doytkać a każda przydzielona do niej osoba po jakimś czasie odchodzi z firmy. Jest to koszt którego przyczyny bardzo trudno dostrzec na pierwszy rzut oka.

To tylko niektóre z metryk, które zastosowałem przy przygotowaniu audytu aplikacji. Nie ma ograniczeń, ile metryk można zdefiniować, ale warto zachować umiar. Możesz łatwo przesadzić i zdefiniować zbyt wiele metryk, co sprawi, że zespół może nie zaangażować się w nie na 100%, a także utrudni proces pomiaru i analizy.

4. "Test oka"

W tym kroku wykonujemy na naszej aplikacji tak zwany "test oka". Niestety nie ma łatwego i prostego przepisu jak to zrobić ponieważ każda aplikacja jest inna, każdy framework jest inny i każdy zespół jest inny. Podzielę się przykładami z aplikacji którą audytowałem które mogą zainspirować cię do sprawdzenia czy podobnych problemów nie ma w Twoim systemie. Nadmienię tylko że aplikacja ta była średniego/dużego rozmiaru i funkncjonowała z powodzeniam na produkcji od kilku lat, więc zanim któś z czytelników stwierdzi "Takie rzeczy nie dzieją się w prawdziwym kodzie" to uwierzcie mi - dzieją się.


Lazy Loading

W taki sposób w jednym z modułów został zaimplementowany lazy loading: 

const routes = [
    {
        path: 'featureA',
        loadChildren: () =>
            import('./modules/feature-a/featureAModule.module').then(
                (mod) => mod.FeatureModuleA,
            ),
        path: 'featureB',
        loadChildren: () =>
            import('./modules/feature-b/featureBModule.module').then(
                (mod) => mod.FeatureModuleB,
            ),
    },
];
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
    declarations: [],
})
export class AppRoutingModule {}


Na pierwszy rzut oka wszystko wygląda dobrze. Spójrzmy jednak na AppModule:

@NgModule({
    declarations: [],
    imports: [FeatureModuleA, FeatureModuleB, FeatureModuleC, AppRoutingModule],
    exports: [],
    providers: [],
    bootstrap: [AppComponent],
})
export class AppModule {}


W tym przykładzie widać że pomimo tego, że mamy poprawnie zdefiniowane leniwe ładowanie naszych modułów to i tak wstrzykujemy je do głównego modułu aplikacji przez co tracimy wszystko co leniwe ładowanie ma nam do zaoferowania. Podczas audytu zadałem pytanie o genezę tego kodu. Okazało się że był on tam od dnia 0 kiedy to w projekcie głównie liczyła się szybkość dostarczania nowych funkcjonalności i nieistotny był lazy loading. Następnie nastąpiło kilka roszad personalnych i tak już zostało zgodnie z zasadą "nie mój kod więc nie dotykam". Jest to przykład probelmu który wystąpił w wyniku kilku czynników a jego naprawa potrwała maksymalnie kilkanaście minut a pozwoliła zredukować bundle size o dobrych kilkaset kB.


Błąd NG0304

Pewnie wiele osób pracujących z Angularem zna ten błąd bardzo dobrze. Pojawia się on gdy chcemy użyć komponentu który nie został poprawnie zaimportowany do naszego modułu. Zazwyczaj kończy się to tym, że dosyć szybko błąd ten namierzamy bo nasz komponent nie działa lub aplikacja w ogóle się nie buduje. Niestety podczas testowania jednostkowego namierzenie tego problemu nie jest już takie oczywiste ponieważ aplikacja uruchomi się poprawnie, oraz nawet wykona testy. Co prawda będzie logowała błędy do konsoli natomiast nic poza tym. Ba, czasem nawet test który wykorzystuje ten komponent będzie oznaczony jako pozytywny. Spójrzmy na poniższy przykład:

To jest template naszego komponentu Home

<!-- kod komponentu home -->
<app-componant-a></app-componant-a>


Jak widzimy importujemy tutaj komponent a. Został on poprawnie zarejestrowany w AppModule i nasza aplikacja działa bez zarzutu. Nasz unit test wygląda w ten sposób:

describe('HomeComponent', () => {
    let component: HomeComponent;
    let fixture: ComponentFixture<HomeComponent>;
    beforeEach(async () => {
        await TestBed.configureTestingModule({
            declarations: [HomeComponent],
        }).compileComponents();
        fixture = TestBed.createComponent(HomeComponent);
        component = fixture.componentInstance;
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});


Tutaj widzimy błąd. Nasz testowy moduł nie importuje komponentu A przez co nie jest w stanie poprawnie się wykonać. Niestety nasze testy nadal oznaczone są jako sukces, pomimo iż został tu zalogowany wyjątek. Poniżej zapi konsoli:

ERROR: 'NG0304: 'app-componant-a' is not a known element (used in the 'HomeComponent' component template):
1. If 'app-componant-a' is an Angular component, then verify that it is a part of an @NgModule where this component is declared.
2. If 'app-componant-a' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.'
  HomeComponent
    √ should create (306ms)


Dokładnie ten przypadek mieliśmy w naszej aplikacji gdzie po zdiagnozowaniu problemu okazało się że około 1% naszych unit testów daje wynik fałszywie pozytywny. Może nie jest to liczba która robi niesamowite wrażenie, ale tak czy inaczej warto wyczulić się na te błędy i upewnić się że każy nasz test robi to co powinien. Aby to osiągnąć doaliśmy te kilka linijek podczas wykonywania naszych testów. 

export function setConsoleWarnAsConsoleError(): void {
    console.error = (message?: string): void => {
        fail(message);
    };
    console.warn = console.error;
}


W ten sposób każdy zalogowanie do konsoli automatycznie zatrzymywało wykonywanie testów przez co nie było możliwośći napisania testu który nie miał poprawnie wstrzykniętych zależności


Featury robione na wyrost

Jeśli chodzi o DX to nie ma jednej dobrej odpowiedzi jak je poprawić. Podam jednak jeden przykład który znaczoąco pozytywnie wpłynął na zadowolenie zespołu a przy tym nic nie zmienił w funkcjonowaniu aplikacji. W naszej aplikacji używaliśmy naszego wewnętrznego systemu do zarządzania treśćią. Dzięki niemu mogliśmy "na żywo" zmieniać zawartość naszej strony bez konieczności publikowania nowej wersji za każdym razem gdy chcemy zmienić tekst na stronie.

Ten sam system chciano zastosować w części dla adminów. Początkowo jednak uznano że jest dużo ważniejszych aspektów które należy rozwijać ale chciano także zostawić sobie furtkę na integracje w przyszłośći. Padł więc pomysł aby używać...pliku json. I tak, sprint po sprincie json rósł, developerzy dodawali i edytowali w nim treść więc nie dość że i tak wymagana była publikacja nowej wersji, to jeszcze zamiast zmieniać tekst bezpośredniu w pliku html, trzeba było szukać go po kluczu w pliku json który w swojej szczytowej formie miał niemal 25 tysięcy linijek.

Po przeprowadzonym audycie okazało się że funkcjonalność prawdziwego CMSa dla panelu admina nie będzie implementowana więc postanowiono rzeczony plik unicestwić i przejść na labelki pisane bezpośrednio w htmlu, dzięki czemu praca z aplikacją stała się o wiele wiele bardziej przyjemna.


Dependency violation

Czasami gdy nasza aplikacja rośnie zaczynamy ją dzielić na moduły i domeny. To bardzo dobra praktyka. Natomiast musimy dokładnie sprawdzić czy nasze domeny nie łamią zasad enkapsulacji. Spójrzmy na przykład poniżej

To jest model z domeny B. Prosta klasa która zawiera kilka propercji

export class DomainBModel {
    name: string;
    count: number;
}


Poniżej widzimy model z domeny A. 

import { DomainBModel } from './domain-b.model';
export class DomainAModel {
    id: number;
    name: string;
    domainB: DomainBModel;
}


Jak widać importujemy tutaj model z domeny B, a jest to bardzo złą praktyką. Wyobraźmy sobie że domena B importuje bardo dużo różnych innych zależności. Przez powyższy kod one wszystkie zostaną zaciągnięte do domeny A przez co enkapsulacja domen zostanie naruszona.

Przykładów może być wiele i nie starczyłoby czasu aby je wszystkie opisać. Ważne jest to, że musimy spojrzeć na naszą aplikacje hollistycznie i przeanalizować wszystkie jej aspekty. Poniżej jeszcze kilka pomocniczych pytań które mogą okazać się pomocne:

  • Czy moja aplikacja dobrze używa reaktywności? Czy nie narażam się na wycieki pamięci?
  • Czy używam najnowszych wersji paczek?
  • Czy do odpowiednich stron ładuję tylko te style które są jej niezbędne czy ładuję wszystkie style z jednego miejsca?
  • Czy moja aplikacja poprawnie używa Typescriptu i wszystkich jego funkcji (jeżeli nasza aplikacja go nie używa to może powinna zacząć?)
  • Czy serwuję kontent renderowany po stronie serwera w miejscach w których liczy się dla mnie performance?

5. Macierz wysiłku/wpływu (Effort/Impact Matrix)

Ten krok jest bardzo istotny ponieważ to on da Ci odpowiedź: od czego mam zacząć. W tym kroku przygotowujemy listę akcji które możemy podjąć aby usprawnić naszą aplikację. Dane czerpiemy z poprzednich kroków i samą listę warto przygotowywać na bieżąco jak tylko zauważymy jakiś problem. Nastepnie do każdej akcji przypisujemy dwie cyfry. Pierwsza będzie oznaczała ile pracy potrzebujemy włożyć w wykonanie jej, a druga określać będzie jaki jest spodziewany wpływ na naszą aplikację. Jak łatwo się domyślić zaczynamy od akcji które wymagają najmniej pracy a dają nam najwięcej wartości. Dla przykładu:

Poprawa sposobu w jaki nasz kod domenowy jest podzielony - zadanie dosyć duże i skomplikowane które może przynieść średnią/dużą wartość

Usunięcie nieużywanego kodu - zadanie dosyć proste o stosunkowo niewielkim wpływie na aplikacje (chyba że kodu tego jest bardzo dużo to wtedy może mieć to znaczny wpływ)

Zaaplikowanie poprawnego lazy loadingu - zadanie dosyć proste które może mieć ogromny wpływ na funkcjonowanie naszej aplikacji

6. Poprawa

Gdy mamy gotową listę możemy zacząć pracę. W tym kroku wykonujemy akcje które zdefiniwaliśmy poprzednio. Oczywiście musi się to odbywać w uzgodnieniu z zespołem kierowniczym ponieważ nie możemy zapominać o codziennej pracy. Warto jednak zaproponować żeby równolegle 'sprzątać' aplikacje. To w jaki sposób do tego podejdziecie bardzo mocno zależy od struktury organizacji, może to być jakaś liczba story pointów co sprint przeznaczona właśnie na tego typu zadania, może to być osobny zespół dedykowany tylko do tego lub dowolne inne rozwiązanie które działa w waszym zespole.

7. Rewizja

Po określonym czasie wykonujemy ponowne sprawdzenie czy nasze metryki się poprawiają. Ważne jest aby robić takie sprawdzenia dosyć regularnie ponieważ chcemy na bieżąco móc reagować jeżeli w jakimś momencie któraś z metryk drastycznie się pogorszy. Jeżeli dobrze wykonaliśmy naszą pracę jest to bardzo satysfakcjonujący moment ponieważ czarno na białym widzimy efekty swojej pracy.

Podsumowanie

Wykonanie audytu technologicznego wymaga dużo pracy i wcześniejszego przygotowania. Jestem jednak przekonany że jest to naprawdę dobry sposób na poznanie, zdiagnozowanie a w konsekwencji uzdrowienie naszej aplikacji. Jest to także idealna podkładka pod rozmowy z zespołem zarządzającym który najczęściej chce wiedzieć jakie benefity otrzymamy jeśli poświęcimy wiele godzin na refactoring kodu.

<p>Loading...</p>