Dashboard Scrum¶

05 de junho, 2024

DashboardIcon.png

Sumário

  • 1. Problema de negócio
  • 2. Proposta de solução
    • 2.1 Ferramentas e custo
  • 3. Configurando a fonte no Airbyte
  • 4. Preparando o banco de dados
    • 4.1 Utilização do Docker
    • 4.2 Criação do Dockerfile
    • 4.3 Criando imagem e container no Docker
    • 4.4 Acessando o banco de dados
  • 5. Configurando o destino no Airbyte
  • 6. Configurando a conexão no Airbyte
  • 7. Realizando transformação nos dados
    • 7.1 Função que retorna a contagem de dias da sprint
    • 7.2 Função que retorna o estimado total da sprint
    • 7.3 Função que determina linha de planejado ideal
    • 7.4 Função que calcula horas restantes
    • 7.5 View
  • 8. Visualizando os dados
    • 8.1 Conexão no Power Bi
    • 8.2 Gráfico Burndown com Tempo Gasto
  • 9. Atualização dos dados
    • 9.1 Comando para deletar view (drop_view.sh)
    • 9.2 Comando para criar view (create_view.sh)
    • 9.3 Comando de agendamento no CRON (crontab)
    • 9.4 Inicializando o CRON no container
    • 9.5 Agendando atualização no Airbyte
  • 10. Conclusão

1. Problema de negócio ¶

Atualmente muitas equipes de TI trabalham com metodologias ágeis como o Scrum e utilizam ferramentas como o Jira Software ou Azure Boards para realizar o controle das atividades durante a Sprint. Essas ferramentas por padrão oferecem alternativas de gráficos para acompanhamento das métricas da Sprint, entretanto caso seja desejado realizar análises mais complexas, envolvendo as regras de negócio de cada empresa, é necessário customizar uma solução.

2. Proposta de solução ¶

Como uma alternativa para essa customização, este projeto visa criar uma base de dados para construção de um gráfico de Burndown, junto com o tempo gasto pelo time, proporcionando uma análise adicional a essa métrica já tão amplamente utilizada para acompanhamento da Sprint.

Para desenvolvimento do projeto foi criado um quadro de amostra no Jira, com dados fictícios simulando uma equipe de TI que utiliza a metodologia Scrum.

Então, a partir dos dados no Jira, será realizado um processo ELT para disponibilizar os dados de maneira a serem utilizados para construção do gráfico final no Power BI, conforme imagem a seguir:

JiraFonte.png

2.1 Ferramentas e custo ¶

Essas ferramentas foram escolhidas por possibilitar a realização do projeto com custo zero, e assim representam uma solução que também pode ser aplicada a empresas de pequeno porte, sem representar nenhum custo de manutenção. Além disso, caso alguma empresa já utilize essas ou ferramentas similares, o projeto pode ser adaptado as especificações e necessidades de cada empresa.

Detalhamento de custo por ferramenta:

  • Jira: gratuito até 10 usuários (https://www.atlassian.com/software/jira/pricing).
  • Airbyte: gratuito para deploy local com algumas limitações, por exemplo, poder ser utilizado por apenas um usuário (https://airbyte.com/pricing).
  • Docker: gratuito até o limite de 250 empregados ou de U$10 milhoẽs em receita anual (https://www.docker.com/pricing/).
  • PostreSQL: totalmente gratuito conforme licença do site oficial (https://www.postgresql.org/about/licence/).
  • PgAdmin: totalmente gratuito conforme licença do site oficial (https://www.pgadmin.org/licence/).
  • Power BI: gratuito para uso local e sem a possibilidade de agendamento (https://powerbi.microsoft.com/en-us/pricing/).

3. Configurando a fonte no Airbyte ¶

Para extrair os dados do Jira é necessário configurá-lo como fonte no Airbyte. Para isso, dentro do Airbyte, em fontes, procurar e selecionar a fonte Jira. Primeiramente é necessário ter o API Token para se conectar ao Jira. Caso a conta do Jira ainda não possua um Token, basta seguir a própria documentação do Jira, contida no Airbyte para adquirir o Token. Assim a configuração poderá ser realizada no Airbyte, com a opção de filtrar projeto e data de início para esta fonte:

JiraFonte.png

4. Preparando o banco de dados ¶

4.1 Utilização do Docker ¶

Os dados serão carregados em um banco de dados Postgresql para serem armazenados e transformados. A fim de ter um ambiente próprio e isolado para a solução, o Postgresql será utilizado a partir de um container Docker. O Docker Desktop é um software de conteinerização seguro e inovador que oferece aos desenvolvedores e equipes um kit de ferramentas híbrido e robusto para criar, compartilhar e executar aplicativos em qualquer lugar.

Para instalar o Docker localmente basta seguir as instruções conforme o sistema operacional em https://www.docker.com/products/docker-desktop/.

O Docker nos permite criar um container totalmente personalizado com os softwares, arquivos e configurações necessárias a partir de um Dockerfile, que irá criar uma imagem no Docker que servirá como template para a criação de containers com as configurações contidas nesse Dockerfile.

4.2 Criação do Dockerfile ¶

Devido ao processo de atualização do Airbyte, que será esclarecido na seção 9, serão necessárias algumas customizações no container, assim para criar o container Docker será utilizada uma imagem personalizada, criada através do seguinte Dockerfile (sem extensão):

# Pegar imagem postgres na versão 16.1 como base
FROM postgres:16.1

# Atualizar e instalar cron e vim
RUN apt-get update && apt-get install -y cron vim

# Definir fuso horário do cron para São Paulo
RUN echo "America/Sao_Paulo" > /etc/timezone

# Definir fuso horário do container para São Paulo
RUN ln -sf /usr/share/zoneinfo/America/Sao_Paulo /etc/localtime

# Copiar o arquivo crontab com as configurações de agendamento
COPY crontab /var/spool/cron/crontabs/root

# Copiar os scripts shell a serem executados pelo crontab
COPY drop_view.sh /tmp
COPY create_view.sh /tmp

# Dar permissão de execução aos arquivos
RUN chmod +x /tmp/drop_view.sh
RUN chmod +x /tmp/create_view.sh

4.3 Criando imagem e container no Docker ¶

Com o arquivo Dockerfile criado, abrir o terminal na pasta em que está o arquivo e executar o comando (obs.: para executar o comando é necessário ter os arquivos drop_view.sh, create_view.sh e crontab criados, sendo seus códigos apresnetados na seção 9):

docker build -t postgres-cron-vim:16.1 .

Em seguida executar o comando abaixo para criação do container, passando as variáveis de ambiente.

docker run -d --name dashboardscrum -p 5579:5432 -e POSTGRES_DB=DashboardScrum -e POSTGRES_USER=user -e POSTGRES_PASSWORD=XXXX postgres-cron-vim:16.1

Obs.: a porta padrão para rodar o Postgresql é a 5432, mas caso na máquina local já esteja rodando o Postgresql ou a porta 5432 já esteja em uso por algum motivo, poderá ser realizado o mapeamento de portas (-p 5579:5432), assim o banco de dados será acessado pela porta 5579 da máquina local e será redirecionado para a porta 5432 dentro do container, onde o Postgresql estará rodando em sua porta padrão.

4.4 Acessando o banco de dados ¶

Para acessar o banco de dados pode ser utilizado o pgAdmin, disponível para download em https://www.pgadmin.org/download/. Neste projeto o pgAdmin foi instalado localmente, mas também é possível que sua instalação e acesso sejam realizados em um container Docker.

Após acessar a tela do pgAdmin, clicar com o botão direito em Servers > Register > Server...

E posteriormente realizar a configuração conforme a porta e as variáveis de ambiente definidas no comando de criação do container.

Conex%C3%A3oPgAdmin-3.png

5. Configurando o destino no Airbyte ¶

Para carregar os dados no Postgresql é necessário configurá-lo como fonte no Airbyte. Para isso, dentro do Airbyte, em destinos, procurar e selecionar o destino Postgres e realizar a configuração conforme as variáveis de ambiente definidas no comando de criação do container:

DBdestino.png

6. Configurando a conexão no Airbyte ¶

Para configurar a conexão no Airbyte, em conexões, selecionar a fonte (Jira) e o destino (Postgres). Na seleção de streams selecionar as tabelas, com os respectivos Sync mode (por padrão já vêm selecionadas algumas outras tabelas que deverão ser desmarcadas):

  • issues - Incremental | Append + Deduped
  • sprint_issues - Incremental | Append + Deduped
  • sprints - Full refresh | Overwrite

Sele%C3%A7%C3%A3oStreamAirbytepng-2.png

Obs.: ainda dentro da seleção de streams é possível selecionar as colunas que serão extraídas de cada tabela, sendo assim, as colunas a serem trazidas podem ser adaptadas conforme a necessidade da empresa.

Posteriormente será configurado o nome, e o agendamento manual para a conexão, para que possa ser realizada a primeira carga no banco de dados.

Conex%C3%A3oAirbyte-2.png

Após configurada a conexão, acessá-la em conexões e clicar em Sync now para realizar a primeira carga no banco de dados.

ConexãoAirbyte.png

7. Realizando transformação nos dados ¶

Para construir o gráfico Burndown serão necessárias as seguintes informações:

  • Sprint;
  • Data;
  • Horas entregues por dia;
  • Tempo gasto por dia;
  • Horas planejadas por dia;
  • Horas restantes por dia (calculado como o total de horas menos o que foi entregue no dia anterior).

A fim de disponibilizar esses dados para serem utilizados no Power BI, será construída uma view contendo essas informações.

Após uma análise exploratória e manipulação dos dados no pgAdmin, ficou determinado a construção de 4 funções para serem utilizadas na formação da view. Os próximos tópicos trazem os respectivos códigos.

7.1 Função que retorna a contagem de dias da sprint ¶

CREATE FUNCTION cont_dias_sprint(sprint_name VARCHAR)
RETURNS SETOF NUMERIC AS $$
-- Selecionado o número de dias da Sprint menos 1 para divisão na coluna de planejado
SELECT COUNT(intervalo) - 1
FROM generate_series(
    -- Gerando série de datas entre o dia de início e dia fim da sprint
    (SELECT "startDate" FROM public.sprints WHERE name = sprint_name),
    (SELECT "endDate" FROM public.sprints WHERE name = sprint_name),
    INTERVAL '1 day'
) AS intervalo
-- Extraindo os dias da semana e filtrando para retirar sábado e domingo
WHERE EXTRACT(dow FROM intervalo) NOT IN (6, 0);
$$ LANGUAGE sql;

7.2 Função que retorna o estimado total da sprint ¶

CREATE FUNCTION estimado_total(sprint_name VARCHAR)
RETURNS SETOF NUMERIC AS $$
-- Somando a estimativa para cada card e trazendo relação com a sprint
SELECT SUM((CAST((i.fields -> 'timeoriginalestimate') AS INTEGER))/3600)
FROM public.issues AS i
JOIN public.sprint_issues AS si ON i.key = si.key
JOIN public.sprints AS s ON si."sprintId" = s.Id
WHERE s.name = sprint_name
GROUP BY s.name;
$$ LANGUAGE sql;

7.3 Função que determina linha de planejado ideal ¶

CREATE OR REPLACE FUNCTION planejado_ideal(sprint_name VARCHAR)
RETURNS TABLE (dia DATE, planejado NUMERIC) AS $$

DECLARE
  -- Declarando variável a ser utilizada no loop começando com o valor estimado total para a sprint 
  current_value NUMERIC(8,6) := estimado_total(sprint_name);
  -- Declarando variável de incremento
  increment_value NUMERIC(8,6) := estimado_total(sprint_name) / cont_dias_sprint(sprint_name);

BEGIN

  FOR dia IN
    -- Loop para percorrer os dias da sprint
    SELECT intervalo
    FROM generate_series(
        (SELECT "startDate" FROM public.sprints WHERE name = sprint_name),
        (SELECT "endDate" FROM public.sprints WHERE name = sprint_name),
        INTERVAL '1 day') AS intervalo
    WHERE EXTRACT(dow FROM intervalo) NOT IN (6, 0)
  LOOP
    -- Começando pelo primeiro valor como o estimado total da sprint
    planejado := current_value;
    RETURN NEXT;
    -- Atualizando próximo valor da coluna sendo o anterior menos o incremento
    current_value := current_value - increment_value;
  END LOOP;

  RETURN;

END;

$$ LANGUAGE plpgsql;

7.4 Função que calcula horas restantes ¶

CREATE OR REPLACE FUNCTION horas_restantes(sprint_name VARCHAR, prev_horas_entregues NUMERIC, datas DATE)
RETURNS TABLE(dia DATE, horas_restantes NUMERIC) AS $$

DECLARE
    -- Definindo cursor para percorrer todos os dias de cada sprint
    tbl_cursor CURSOR FOR (
        SELECT s.name AS sprint, data.data :: DATE
        FROM public.sprints AS s
        CROSS JOIN generate_series(s."startDate", s."endDate", interval '1 day') AS data
        WHERE EXTRACT(dow FROM data.data) NOT IN (6, 0) AND s.name = sprint_name
        ORDER BY data
    );
    tbl_row RECORD;
    -- Definindo valor inicial como estimado total para a sprint
    horas_restantes NUMERIC := estimado_total(sprint_name);
    -- Definindo variáveis de controle para que a cada iteração possam ser selecionadas as horas entregues do dia anterior
    horas_entregues NUMERIC := 0;
    current_value NUMERIC := 0;

BEGIN

    OPEN tbl_cursor;

    LOOP
        -- Definindo critério de parada para o cursor
        FETCH FROM tbl_cursor INTO tbl_row;
        EXIT WHEN NOT FOUND;
        -- Selecionando as horas entregues
        SELECT COALESCE(SUM((issues.fields ->> 'timeoriginalestimate')::INTEGER / 3600), 0)
        INTO horas_entregues
        FROM issues
        WHERE to_date(issues.fields ->> 'resolutiondate', 'YYYY-MM-DD') = tbl_row.data;
        -- Subtraindo o valor da coluna horas restantes pelo valor de horas entregues
        horas_restantes := horas_restantes - current_value;
        -- Armazenando o valor de horas entregues na data x, para que na próxima interação seja feita a 
        -- subtração por esse valor, que representará o valor de horas entregues na data anterior (x-1)
        current_value := horas_entregues;
        -- Retornando a data e o valor de horas_restante a cada iteração
        RETURN QUERY SELECT tbl_row.data, horas_restantes;
    END LOOP;

    CLOSE tbl_cursor;

END;

$$ LANGUAGE plpgsql;

7.5 View ¶

CREATE OR REPLACE VIEW public.vw_dadosburndown AS
WITH dados_sprint AS (
    SELECT 
        s.name AS sprint, 
        data.data :: DATE
    FROM public.sprints AS s
    CROSS JOIN generate_series(s."startDate", s."endDate", INTERVAL '1 day') AS data
    WHERE EXTRACT(dow FROM data.data) NOT IN (6, 0)
    ORDER BY data
),
dados_horas_entregues AS(
    SELECT 
        SUM((CAST((fields -> 'timeoriginalestimate') AS INTEGER))/3600) AS horas_entregues,
        to_date(fields ->> 'resolutiondate', 'YYYY-MM-DD') AS data_encerramento
    FROM public.issues
    GROUP BY data_encerramento
),
prev_dados_tempo_gasto AS(
    SELECT
        CAST((jsonb_array_elements(fields -> 'worklog' -> 'worklogs') -> 'timeSpentSeconds') AS INTEGER)/3600 AS tempo_gasto,
        to_date(jsonb_array_elements(fields -> 'worklog' -> 'worklogs') ->> 'updated', 'YYYY-MM-DD') AS dia_gasto
    FROM public.issues
),
dados_tempo_gasto AS(   
    SELECT
        dia_gasto,
        SUM(tempo_gasto) AS tempo_gasto
    FROM prev_dados_tempo_gasto
    GROUP BY dia_gasto
)
SELECT
    ds.sprint,
    ds.data,
    dhe.horas_entregues,
    dtg.tempo_gasto,
    plan.planejado,
    he.horas_restantes
FROM dados_sprint AS ds
LEFT JOIN LATERAL planejado_ideal(ds.sprint) AS plan ON ds.data = plan.dia
LEFT JOIN dados_horas_entregues AS dhe ON ds.data = dhe.data_encerramento
LEFT JOIN dados_tempo_gasto AS dtg ON ds.data = dtg.dia_gasto
JOIN horas_restantes(ds.sprint, dhe.horas_entregues, ds.data) AS he ON ds.data = he.dia
ORDER BY ds.sprint,ds.data;

8. Visualizando os dados ¶

No pgAdmin a view retorna a seguinte tabela com os dados. A seguir será realizada a conexão do Postgresql com o Power Bi para construção do gráfico Burndown com tempo gasto.

DadosView.png

8.1 Conexão no Power Bi ¶

No Power Bi deverá ser acessada a fonte "Banco de dados PostgreSQL". Para o projeto em questão o Power BI foi acessado por outra máquina, assim no servidor é preciso configurar o IP, e a porta (caso tenha sido mapeada uma porta diferente na criação do container):

ConexãoPowerBI_1.png

Posteriormente colocar usuário e senha conforme as variáveis de ambiente definidas no comando de criação do container:

ConexãoPowerBI_2.png

E por fim selecionar a view criada entre as tabelas disponíveis:

ConexãoPowerBI_3.png

8.2 Gráfico Burndown com Tempo Gasto ¶

Burndown.png

Como pode se observar no gráfico, para a sprint em questão, as entregas estavam atrasadas (horas restantes acima da linha de planejado) até o dia 10/06. Então verifica-se um registro de tempo gasto maior nesse dia e no seguinte, o que pode representar a melhora no desempenho nos últimos dias da sprint. Além disso, outra vantagem de acompanhar o registro de tempo gasto por dia pode ser o auxilio na maturidade do time, quando ainda não se tem um bom controle de horas.

9. Atualização dos dados ¶

Para realizar a atualização dos dados é possível agendá-la no Airbyte. Entretanto, para este projeto será necessária uma pequena adaptação. A tabela "sprints" não possui uma coluna que possibilitaria realizar o apend incremental dos dados, portanto o seu modo de sincronização é o "Full refresh | Overwrite". Neste modo o Airbyte dropa (deleta) a tabela para então construí-la novamente com os dados atualizados. Porém, como a tabela "sprints" é utilizada na view no bando de dados, ela não pode ser deletada. Para contornar essa questão, a solução proposta foi agendar via CRON no container o drop e criação da view e a atualização do Airbyte ser realizada entre esses dois comandos.

Por isso na criação do container foram copiados os arquivos drop_view.sh, create_view.sh e crontab que representam, respectivamente, os arquivos com os comando para deletar a view, criar a view e o agendamento no CRON. Os próximos tópicos trazem esses códigos.

9.1 Comando para deletar view (drop_view.sh) ¶

#!/bin/bash

# Definindo as variáveis de ambiente para o PostgreSQL
export PGHOST=localhost
export PGUSER=user
export PGPASSWORD=XXXXX
export PGDATABASE=DashboardScrum

# Executando o comando para deletar a view
psql -c "DROP VIEW public.vw_dadosburndown;"

9.2 Comando para criar view (create_view.sh) ¶

#!/bin/bash

# Definindo as variáveis de ambiente para o PostgreSQL
export PGHOST=localhost
export PGUSER=gpb
export PGPASSWORD=gpb5579
export PGDATABASE=DashboardScrum

# Váriavél com a criação da view
VIEW_DEFINITION="
CREATE OR REPLACE VIEW public.vw_dadosburndown AS
WITH dados_sprint AS (
  SELECT 
      s.name AS sprint, 
      data.data :: DATE
  FROM public.sprints AS s
  CROSS JOIN generate_series(s.\"startDate\", s.\"endDate\", INTERVAL '1 day') AS data
  WHERE EXTRACT(dow FROM data.data) NOT IN (6, 0)
  ORDER BY data
),
dados_horas_entregues AS(
  SELECT 
    SUM((CAST((fields -> 'timeoriginalestimate') AS INTEGER))/3600) AS horas_entregues,
    to_date(fields ->> 'resolutiondate', 'YYYY-MM-DD') AS data_encerramento
  FROM public.issues
  GROUP BY data_encerramento
),
prev_dados_tempo_gasto AS(
  SELECT
    CAST((jsonb_array_elements(fields -> 'worklog' -> 'worklogs') -> 'timeSpentSeconds') AS INTEGER)/3600 AS tempo_gasto,
    to_date(jsonb_array_elements(fields -> 'worklog' -> 'worklogs') ->> 'updated', 'YYYY-MM-DD') AS dia_gasto
  FROM public.issues
),
dados_tempo_gasto AS( 
  SELECT
    dia_gasto,
    SUM(tempo_gasto) AS tempo_gasto
  FROM prev_dados_tempo_gasto
  GROUP BY dia_gasto
)
SELECT
  ds.sprint,
  ds.data,
  dhe.horas_entregues,
  dtg.tempo_gasto,
  plan.planejado,
  he.horas_restantes
FROM dados_sprint AS ds
LEFT JOIN LATERAL planejado_ideal(ds.sprint) AS plan ON ds.data = plan.dia
LEFT JOIN dados_horas_entregues AS dhe ON ds.data = dhe.data_encerramento
LEFT JOIN dados_tempo_gasto AS dtg ON ds.data = dtg.dia_gasto
JOIN horas_restantes(ds.sprint, dhe.horas_entregues, ds.data) AS he ON ds.data = he.dia
ORDER BY ds.sprint,ds.data;"

# Executando o comando de criação da view
psql -c "$VIEW_DEFINITION"

9.3 Comando de agendamento no CRON (crontab) ¶

0 3 * * 1-5 /tmp/drop_view.sh >> /tmp/cron_drop.log 2>&1
0 5 * * 1-5 /tmp/create_view.sh >> /tmp/cron_create.log 2>&1

Dessa forma os scripts serão executados às 3:00h e 5:00h da manhã de segunda a sexta-feira, e sua saídas/erros serão gravadas nos arquivos cron_drop.log e cron_create.log dentro da pasta tmp do container.

Obs.: utilizando >> serão gravados todos os logs. Utilizando > será gravado apenas o log da útlima execução.

9.4 Inicializando o CRON no container ¶

Para ativar os comandos do CRON é necessário entrar no terminal do container e, estando na linguagem bash, executar o seguinte comando:

service cron start

Para verificar se o CRON está de fato rodando pode ser utilizado o comando:

service cron status

9.5 Agendando atualização no Airbyte ¶

Por fim é necessário agendar a atualização no Airbyte, que pode ser feita em Settings, na conexão, conforme a imagem a seguir:

Autalização Airbyte.png

Dessa forma, às 3:00h a view será deletada do banco de dados, às 04:00h o Airbyte irá realizar a atualização do bando de dados e às 5:00h a view será recriada, deixando os dados disponíveis para serem atualizados no Power BI normalmente. Esta é apenas uma sugestão de configuração, os horários podem ser alterados a depender das necessidades da empresa.

10. Conclusão ¶

O gráfico de Burndown com tempo gasto é apenas um exemplo do que pode ser feito obtendo os dados brutos do Jira e transformando-os, sendo possível adaptar essa transformação conforme as necessidade e regras de negócio de cada empresa. Vale lembrar que existem plug-ins e aplicativos que podem ser adicionados ao Jira para gráficos personalizados, entretanto nem sempre são gratuitos e trazendo a solução dessa forma, a flexibilidade tanto para construir os gráficos, quanto para implementar as regras de negócio da empresa é muito maior.

Entre em contato¶

Caso tenha alguma dúvida, sugestão ou apenas queira trocar uma ideia sobre este projeto, fico à disposição.

E-mail: gabrielapbarros15@gmail.com

LinkedIn: https://www.linkedin.com/in/gabriela-pereira-barros/