Data Lake (BI)
Como consumir os dados da Fhinck no BigQuery: acesso, ferramentas de BI, boas práticas e arquitetura recomendada.
Qualquer regra de negócio adicional, transformação, enriquecimento, agregação, modelagem, métricas e dashboards criados a partir dos dados disponibilizados pela Fhinck são de responsabilidade integral do cliente/parceiro.
A Fhinck disponibiliza o acesso aos dados e às tabelas padronizadas, mas não se responsabiliza por resultados obtidos a partir de consultas customizadas, nem por decisões tomadas com base em transformações implementadas fora do escopo do dashboard padrão.
Índice
Visão geral
A Fhinck disponibiliza aos clientes/parceiros um projeto e dataset exclusivo no Google Cloud Platform (GCP), contendo tabelas padronizadas e prontas para consumo em uma arquitetura de Data Lake / Data Warehouse (BigQuery).
O acesso normalmente é concedido por uma Conta de Serviço (Service Account) com permissões de leitura nas tabelas do dataset.
Como os dados são disponibilizados no BigQuery
- A base contém tabelas de eventos (dados brutos) e tabelas relacionais (cadastros/atributos organizacionais).
- O BigQuery permite que o cliente execute consultas SQL diretamente nas tabelas e conecte em ferramentas de BI.
- Se a organização do cliente não tiver usuários Google com acesso direto, a autenticação pode ser feita via Service Account (chave JSON).
Atualização dos dados e consumo mensal
Frequência/latência de atualização
A frequência (e o tempo de atualização) das tabelas disponibilizadas para consumo em BI varia conforme o tipo de licença contratada pelo cliente.
O cliente deve considerar essa característica ao definir janelas de refresh no Power BI (por exemplo, refresh diário, múltiplas vezes ao dia, etc.), alinhando com a licença contratada.
Limite de consumo (10 TB/mês)
O contrato de BI prevê um consumo mensal de referência de 10 TB/mês.
- Se o cliente ultrapassar esse consumo, a Fhinck irá notificar para que seja realizada a regularização (adequação de plano/escopo) conforme contrato.
- Para evitar consumo excessivo, recomenda-se otimizar consultas, reduzir varreduras desnecessárias e utilizar tabelas/vistas derivadas quando aplicável.
Ferramentas de BI suportadas
Os dados podem ser consumidos por praticamente qualquer ferramenta moderna de BI, usando conectores nativos ou drivers (ODBC/JDBC) do BigQuery.
Acesso (Service Account) e quando é disponibilizado
A Fhinck fornece o acesso ao Data Lake via Conta de Serviço (Service Account) do Google Cloud após a contratação da licença de BI.
O cliente recebe:
- E-mail da Service Account (formato:
empresa@fnk-bi-empresa.iam.gserviceaccount.com) - Arquivo de chave JSON (credencial)
Importante: a chave JSON deve ser tratada como segredo. Ela dá acesso de leitura ao dataset do cliente no BigQuery e não deve ser compartilhada em canais não seguros.
Passo a passo: conectar no Microsoft Power BI (com JSON)
Pré-requisitos
- Power BI Desktop instalado.
- Service Account (e chave JSON) já entregue pela Fhinck.
- Projeto e dataset do cliente já liberados para a Service Account.
1) Selecionar a fonte
- No Power BI Desktop: Página Inicial → Obter dados → Google BigQuery.
2) Autenticar via Service Account
- Quando o Power BI solicitar credenciais, selecione Service Account Login.
- Informe:
- O e-mail da Service Account.
- O conteúdo do arquivo JSON (ou selecione o arquivo, dependendo da tela/versão do Power BI).
3) Escolher projeto/dataset e tabelas
- No navegador do conector, selecione:
- O projeto no GCP.
- O dataset.
- As tabelas desejadas (por exemplo
eventse tabelas relacionais).
4) Definir modo de consumo (Import vs DirectQuery)
- Import: carrega dados para o PBIX. Boa performance, mas exige refresh (manual/agendado).
- DirectQuery: consulta o BigQuery a cada interação. Recomendado quando o volume é alto e você quer manter o dado na nuvem.
5) Publicar e configurar atualização (Power BI Service)
- Após publicar no Power BI Service, configure as credenciais do dataset para manter o refresh.
- Se o ambiente exigir (política corporativa), pode ser necessário configurar um gateway.
Conectar no Looker Studio
- Criar uma nova fonte de dados.
- Selecionar conector BigQuery.
- Autenticar com conta Google que tenha acesso ao dataset ou usar a Service Account conforme a política do cliente.
Outras plataformas
- Tableau (conector nativo BigQuery)
- Qlik (ODBC/JDBC, driver Simba)
- Metabase, Redash, Superset e outras ferramentas compatíveis com SQL/JDBC/ODBC
Em todas as ferramentas, a regra é a mesma: a credencial (JSON) autentica e as permissões IAM determinam quais projetos/datasets/tabelas serão visíveis.
Modelo recomendado para Data Lake corporativo (Cloud Storage + Parquet)
As seções anteriores cobrem o consumo dos dados em ferramentas de BI (Power BI, Looker Studio, Tableau, etc.). Para alimentar um Data Lake corporativo com extrações volumosas e recorrentes (Snowflake, Oracle, Databricks, Postgres, Redshift, ou BigQuery do próprio cliente), o modelo de extração via JDBC síncrono tradicional não é o mais indicado. Recomendamos o pipeline desacoplado descrito abaixo.
Por que extração JDBC síncrona não escala para Data Lake
- A maioria dos drivers JDBC opera em modo síncrono, sujeito ao limite de 128 MB compactados por resposta da Query API do BigQuery (erro
responseTooLarge). - Uma extração diária da tabela de eventos pode passar facilmente de dezenas de GB descompactados, mesmo em janelas curtas (3 dias úteis costumam render 60M+ linhas).
- Mesmo abaixo do limite, o transporte JDBC paginado serializa dezenas de milhares de chamadas HTTP, criando timeouts e fragilidade de rede.
Pipeline desacoplado (BigQuery EXPORT DATA → Cloud Storage)
O BigQuery exporta o snapshot diário em Parquet com compressão Snappy para um bucket dedicado no Cloud Storage. O Data Lake do cliente consome o arquivo, sem dependência de rede ou JDBC. Vantagens objetivas:
- Tamanho final ~30× menor que JSON descompactado (formato colunar comprimido)
- Idempotente e retomável: falhas de rede ou processo não exigem repetir a extração
- Sem limite de 128 MB e sem dependência de janela síncrona
- Custo de extração no BigQuery da ordem de R$ 3,00/dia para uma janela típica de 3 dias úteis
Ferramentas de mercado para consumir Cloud Storage
Do lado do consumidor (Data Lake, Oracle, Snowflake, Postgres, etc.), há diversas opções consolidadas:
- Apache NiFi (open-source, on-prem) — processadores nativos
FetchGCSObject+PutDatabaseRecord. Comum em corporações, especialmente em ambientes regulados (saúde, financeiro). - Suítes ETL comerciais (Pentaho Data Integration, Talend, Informatica, IBM DataStage) — conectores nativos para Google Cloud Storage e qualquer destino.
- Apache Airflow + Python — combinação
google-cloud-storage+pyarrow+ driver SQL (oracledb,psycopg,pyodbc). Tipicamente em torno de 50 linhas de código. - Apache Spark — leitura Parquet nativa para volumes maiores, escrita JDBC para o destino.
- gsutil / gcloud storage + script de carga — opção mais simples para casos pontuais ou enquanto a automação não está pronta.
Como solicitar o pipeline com a Fhinck
A Fhinck disponibiliza o pipeline (job EXPORT DATA agendado + bucket regional dedicado + Cloud Scheduler + IAM read-only para a Service Account do cliente) em até 5 dias úteis após alinhamento técnico. Solicitação por meio do canal de CSM ou Slack do cliente.
Padrões e anti-padrões em extração de dados
Recomendações práticas para evitar os erros mais comuns observados em extrações contra o BigQuery, válidas tanto para o consumo via BI quanto para alimentação de Data Lake.
Padrões recomendados
- Para qualquer query cuja resposta possa exceder 128 MB, configure
allowLargeResults=true+destinationTable=<dataset.tabela_temp>. A maioria das ferramentas ETL tem essa opção no módulo de carga ou nas opções de conexão. - Habilite a Storage Read API no driver JDBC (parâmetro
EnableHighThroughputAPI=1no driver Simba/Google JDBC for BigQuery). Streaming binário gRPC, 50-100× mais rápido que o modo síncrono, sem limite de 128 MB. Requer o papel IAMroles/bigquery.readSessionUserna Service Account — concedido pela Fhinck mediante solicitação. - Filtre pela coluna de particionamento da tabela. Em tabelas de eventos da Fhinck, a coluna canônica é
p2o_datetime_in. Filtros aplicados a outras colunas de data (por exemplo,p2o_datetime_last_insert) não atingem o particionamento e ampliam significativamente o volume escaneado. - Liste explicitamente as colunas necessárias em vez de
SELECT *. Tabelas de eventos têm 100+ colunas; a maioria dos casos de uso consome 10-20.
Anti-padrões a evitar
SELECT *sem cláusulaWHEREem tabela de eventos — gera scan completo da tabela, frequentemente acima de 1 TB.- Filtros de data aplicados a colunas não-particionadas — não reduzem o volume escaneado, mesmo que a janela de datas seja pequena.
- Submissão de jobs de extração sem
destinationTableem queries que retornam mais de 128 MB — causa o erroresponseTooLarge, frequentemente confundido com timeout de rede. - Misturar dialeto Oracle/PL-SQL com BigQuery — o BigQuery utiliza GoogleSQL (Standard SQL), com diferenças importantes em qualificação de tabelas (crases e três níveis
`projeto.dataset.tabela`), tipos de data (TIMESTAMP,DATETIMEeDATEsão distintos), e funções de timestamp.
Boas práticas (Data Lake)
Para manter governança, rastreabilidade e performance, recomenda-se:
- Evitar aplicar regras complexas somente na camada do BI.
- Preferir views/tabelas derivadas no BigQuery, com versionamento de código e trilha de auditoria.
- Criar camadas (ex.: raw → staging → curated/marts) conforme a arquitetura do cliente.
Regras de negócio e customizações (modelo recomendado)
Quando o cliente precisar aplicar regras de negócio próprias (ou replicar regras do dashboard Fhinck com adaptações), a recomendação é criar um script agendado (job), alinhado a boas práticas de data lakes.
Esse job deve:
- Executar uma consulta SQL (ou conjunto de consultas) no BigQuery.
- Aplicar as regras de negócio e transformações necessárias.
- Gravar o resultado em tabelas/vistas do cliente (camada curada), que serão então consumidas pelo Power BI/Looker/Tableau.
Por que assim?
- Centraliza e padroniza a regra (uma fonte única de verdade).
- Evita duplicação de lógica em múltiplos relatórios.
- Facilita auditoria, controle de versão e troubleshooting.
- Melhora performance e estabilidade para consumo em BI.
Conta de serviço e tabelas padrão
A conta de serviço possui acesso a um projeto e dataset exclusivos no GCP, com tabelas padrão:
- events: eventos brutos coletados das máquinas e enviados para a nuvem.
- rel_hierarchy_employee_associative: cadastro de colaboradores (configuração mais recente por login).
- rel_hierarchy_domain_associative: cadastro/classificação de domínios por nível de departamento.
- rel_department_hierarchy: cadastro/hierarquia de departamentos.
- rel_region_attribute: cadastro de regiões.
- rel_role_attribute: cadastro de cargos.
- tabelas derivadas/curadas: tabelas que o próprio cliente pode criar no BigQuery a partir dos dados brutos, aplicando regras de negócio e preparando a camada de consumo para BI.
Dicionário de dados (schema resumido)
Padrão: tabelas no projeto fnk-bi-{cliente}, dataset {company}. Convenção: {nome_tabela}_{company}.
- events_{company} (eventos brutos do agente desktop da Fhinck)
bigquery_id(STRING) — ID único do eventop2o_username(STRING) — email/username do colaboradorp2o_hostname(STRING) — nome da máquinap2o_datetime_in(TIMESTAMP) — início do evento (janela em foco)p2o_datetime_out(TIMESTAMP) — fim do eventop2o_total_seconds(FLOAT64) — duração total (segundos)p2o_total_idle(FLOAT64) — tempo ocioso dentro do eventop2o_app_exe(STRING) — executável (ex.:chrome.exe)p2o_app_exe_fullname(STRING) — nome completo do appp2o_title_windows(STRING) — título da janela ativap2o_url(STRING) — URL (quando navegador)p2o_url_domain(STRING) — domínio da URLp2o_network(STRING) — informações de rede (SSID etc.)p2o_mouse_hooks(STRING) — dados de atividade do mousep2o_keyboard_hooks(STRING) — dados de atividade do tecladop2o_IsWorkstationLocked(BOOLEAN) — estação bloqueadap2o_source(STRING) — origem do dadop2o_datetime_last_insert(TIMESTAMP) — timestamp da última inserçãoodd_behavior(STRING) — flags de comportamento anômalo
- rel_department_hierarchy_{company} (hierarquia de departamentos)
department_name(STRING) — nome do departamentodepartment_code(STRING) — código do departamentocreation_user(STRING) — usuário que criou o registrocreation_date(TIMESTAMP) — data de criaçãoactive(BOOLEAN) — se está ativodepartment_level(INT64) — nível hierárquico (1=topo, 2=sub, etc.)
- rel_role_attribute_{company} (cargos/funções)
role_name(STRING) — nome do cargorole_code(STRING) — código do cargocreation_user(STRING) — usuário que criou o registrocreation_date(TIMESTAMP) — data de criaçãoactive(BOOLEAN) — se está ativo
- rel_region_attribute_{company} (regiões/localidades)
region_name(STRING) — nome da regiãoregion_code(STRING) — código da regiãocreation_user(STRING) — usuário que criou o registrocreation_date(TIMESTAMP) — data de criaçãoactive(BOOLEAN) — se está ativa
- rel_hierarchy_employee_associative_{company} (vínculo colaborador × hierarquia)
fk_department_code(STRING) — código do departamentofk_process_code(STRING) — código do processofk_role_code(STRING) — código do cargofk_region_code(STRING) — código da regiãoemployee(STRING) — email/username do colaboradoremployee_alias(STRING) — nome de exibiçãodepartment_path(STRING) — caminho hierárquico (códigos separados por_)process_path(STRING) — caminho de processosvacation_active(BOOLEAN) — se está em fériasvacation_date_in(TIMESTAMP) — início das fériasvacation_date_out(TIMESTAMP) — fim das fériascreation_user(STRING) — usuário que criou o registrocreation_date(TIMESTAMP) — data de criaçãosearch_date(TIMESTAMP) — data de vigência da associaçãoactive(BOOLEAN) — se a associação está ativaEntry_Weekday(STRING) — horário de entrada (dia útil)Exit_Weekday(STRING) — horário de saída (dia útil)Entry_Saturday(STRING) — horário de entrada (sábado)Exit_Saturday(STRING) — horário de saída (sábado)Entry_Sunday(STRING) — horário de entrada (domingo)Exit_Sunday(STRING) — horário de saída (domingo)
- rel_hierarchy_domain_associative_{company} (domínios/apps × hierarquia)
fk_department_code(STRING) — código do departamentofk_process_code(STRING) — código do processofk_role_code(STRING) — código do cargofk_region_code(STRING) — código da regiãodomain(STRING) — domínio ou aplicativocategory(STRING) — categoria do domíniodepartment_path(STRING) — caminho hierárquicoprocess_path(STRING) — caminho de processoscreation_user(STRING) — usuário que criou o registrocreation_date(TIMESTAMP) — data de criaçãosearch_date(TIMESTAMP) — data de vigênciaactive(BOOLEAN) — se a associação está ativawaterfall_classification(STRING) — classificação (produtivo, neutro, improdutivo)
- time_extension_request_{company} (solicitações de extensão de jornada)
request_date(DATE) — data da solicitaçãorequest_timestamp(TIMESTAMP) — timestamp exato da solicitaçãocompany(STRING) — empresausername(STRING) — colaborador solicitantetime_requested(INT64) — tempo extra solicitado (minutos)dh_insert(TIMESTAMP) — data/hora de inserção no BQaccepted(BOOLEAN) — se foi aceitaaccepted_timestamp(TIMESTAMP) — quando foi aceita/rejeitadamanager_accepted(STRING) — gestor que aprovou/rejeitoudate_limit(DATE) — data limite da extensãojustification(STRING) — justificativaoriginal_requested(INT64) — tempo originalmente solicitadorequest_manager(STRING) — gestor para quem foi enviada
- time_tracking_{company} (registros de ponto)
action_date(DATE) — data do registroaction_id(STRING) — ID da açãousername(STRING) — colaboradorhostname(STRING) — máquina de origembeginDay(TIMESTAMP) — início da jornadabeginLunch(TIMESTAMP) — início do almoçoendLunch(TIMESTAMP) — fim do almoçoendDay(TIMESTAMP) — fim da jornadasource(STRING) — origem (manual, sistema, etc.)action_datetime_sent(TIMESTAMP) — quando foi enviadoaction_datetime_bq_insert(TIMESTAMP) — quando foi inserido no BQlocationType(STRING) — tipo de local (escritório, home, etc.)lastUpdate(TIMESTAMP) — última atualização
- dim_employee_daily_{company} (dimensão de colaborador por dia)
employee(STRING) — email/username (lowercase)p2o_date(DATE) — data de referênciaemployee_alias(STRING) — nome de exibição (uppercase)role_name(STRING) — cargo vigente na dataRegion(STRING) — região vigente na datadepartment_name(STRING) — departamento mais profundoDepartment_1(STRING) — depto nível 1Department_2(STRING) — depto nível 2Department_3(STRING) — depto nível 3Department_4(STRING) — depto nível 4Department_5(STRING) — depto nível 5Department_6(STRING) — depto nível 6Department_7(STRING) — depto nível 7Department_8(STRING) — depto nível 8Department_9(STRING) — depto nível 9Department_10(STRING) — depto nível 10 (mais profundo)
Tabelas de suporte
- network_raw_latest (redes corporativas)
DATA(STRING/JSON) — documento com$.network(array de SSIDs corporativos) e$.company
- text_clearing_rules (regras de limpeza/anônimização)
regex_rule(STRING) — expressão regularreplace_value(STRING) — valor de substituiçãoactive(BOOLEAN) — se está ativacompany(STRING) — empresa ouGENERAL_ALL
- app_classificationV2 (regras de classificação de apps/sites)
company(STRING) — empresa ouGENERAL_ALLapp_name_en(STRING) — nome do app (inglês)regex_rule(STRING) — regex para matching em título/URLapp_regex(STRING) — regex adicionalapp_class(STRING) — classe do appapp_class_id(INT64) — ID numérico da classernk(INT64) — prioridade da regraactive(BOOLEAN) — se está ativa
UDFs (User Defined Functions)
clean_textV2(input, options, replacements)— função JS para limpeza/normalização de títulosget_app_classification(rules, search_strings, app_web)— função JS para classificação de aplicativos
Consulta de referência (para aplicar regras e preparar a camada curada)
A consulta abaixo pode ser usada como base para o job agendado do cliente.
dim_employee_daily_{company} vêm employee_alias, department_name, role_name, region_name e Department_1..10, com os valores vigentes na data do evento (histórico correto); de rel_hierarchy_employee_associative_{company} vêm fk_department_code, fk_role_code, fk_region_code, fk_process_code, department_path e process_path, que refletem o cadastro atual — esse cadastro é um snapshot por login, sem versionamento. Para análise histórica por departamento ou cargo, use os campos de nome; os códigos servem para integrar com cadastros seus. Aderência verificada em dia útil (900.789 linhas): nenhuma linha sem departamento, cargo ou nome, e zero divergência de departamento, cargo e região nos 342 colaboradores.AND e.p2o_total_seconds > 0 no CTE filtered_events — sem ela, eventos de duração zero (sinal de estação bloqueada) sobrevivem ao filtro, porque 0 >= 0 é verdadeiro, e a extensão de intervalo os converte em tempo de atividade. (2) Janela de jornada: a consulta passou a montar a jornada por colaborador a partir dos eventos desbloqueados (CTEs unlocked_seq … journey_by_day), a manter somente eventos dentro da janela [entrada, saída] e a usar a data de entrada da jornada como p2o_date — em vez do dia-calendário do evento. Tempo de máquina ligada fora da jornada não é atividade.
WITH
config AS (
SELECT
'EMPRESA' AS company_name,
DATE_SUB(CURRENT_DATE(), INTERVAL 15 DAY) AS date_start,
DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) AS date_end
),
corporate_networks AS (
SELECT
ARRAY_AGG(DISTINCT LOWER(TRIM(REPLACE(network, '"', '')))) AS networks
FROM `fnk-bi-empresa.EMPRESA.network_raw_latest`,
UNNEST(JSON_EXTRACT_ARRAY(DATA, "$.network")) AS network
WHERE JSON_EXTRACT_SCALAR(DATA, "$.company") = 'EMPRESA'
),
text_clearing_rules AS (
SELECT
ARRAY_AGG(STRUCT(
regex_rule AS regex,
replace_value AS replacement
)) AS clearing_array
FROM `fnk-bi-empresa.EMPRESA.text_clearing_rules`
WHERE active = TRUE
),
app_classification_rules AS (
SELECT
ARRAY_AGG(STRUCT<
company STRING,
app_name STRING,
regex_rule STRING,
app_regex STRING,
app_class STRING,
app_class_id INT64,
rnk INT64
>(
company,
app_name_en,
regex_rule,
app_regex,
app_class,
app_class_id,
rnk
) ORDER BY
CASE WHEN company != 'GENERAL_ALL' THEN 0 ELSE 1 END,
CASE WHEN app_class = 'web_browser__class' THEN 1 ELSE 0 END,
rnk
) AS rules_array
FROM `fnk-bi-empresa.EMPRESA.app_classificationV2`
WHERE active = TRUE
AND (company = 'EMPRESA' OR company = 'GENERAL_ALL')
),
filtered_events AS (
SELECT
e.*,
DATE(e.p2o_datetime_in) AS p2o_date
FROM `fnk-bi-empresa.EMPRESA.events_EMPRESA` e,
config c
WHERE
DATE(e.p2o_datetime_in) BETWEEN c.date_start AND c.date_end
AND LOWER(e.p2o_username) != 'fhinck'
AND e.p2o_total_seconds >= e.p2o_total_idle
AND e.p2o_total_seconds > 0
AND (e.p2o_source IS NULL OR UPPER(IFNULL(e.p2o_source, '')) != 'CORRUPTED')
AND (
e.odd_behavior IS NULL
OR (
e.odd_behavior NOT LIKE '%Invalid total_seconds%'
AND e.odd_behavior NOT LIKE '%Unexpected Time Change%'
AND e.odd_behavior NOT LIKE '%Corrupted File%'
)
)
),
windowed_events AS (
SELECT
*,
LAG(p2o_title_windows, 8) OVER w AS lag_title_windows,
LEAD(p2o_title_windows, 8) OVER w AS lead_title_windows,
COUNTIF(p2o_title_windows = 'LockingWindow')
OVER (w ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) -
IF(p2o_title_windows = 'LockingWindow', 1, 0) AS cnt_prc_lockwindow,
COUNTIF(p2o_title_windows = 'LockingWindow')
OVER (w ROWS BETWEEN CURRENT ROW AND 7 FOLLOWING) -
IF(p2o_title_windows = 'LockingWindow', 1, 0) AS cnt_flw_lockwindow,
LAG(p2o_url) OVER w AS lag_p2o_url,
LEAD(CONCAT(IFNULL(p2o_app_exe, ''), IFNULL(p2o_url, ''))) OVER w AS lead_app_url
FROM filtered_events
WINDOW w AS (PARTITION BY p2o_username, p2o_hostname ORDER BY p2o_datetime_in, p2o_datetime_out)
),
deduplicated_events AS (
SELECT * EXCEPT(row_num)
FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY bigquery_id
ORDER BY p2o_datetime_last_insert DESC, p2o_datetime_in DESC
) AS row_num
FROM windowed_events
)
WHERE row_num = 1
),
communication_detection AS (
SELECT
*,
CASE
WHEN REGEXP_CONTAINS(LOWER(IFNULL(p2o_app_exe_fullname, p2o_app_exe)),
r'(teams|microsoft.*teams|zoom|skype|webex|cisco.*webex|google.*meet|gotomeeting|bluejeans|jabber|cisco.*jabber|chime|amazon.*chime|hangouts|google.*hangouts|ringcentral|8x8|fuze|avaya|polycom|lifesize|starleaf|pexip|vidyo|adobe.*connect|logmein|citrix.*gotomeeting|join\.me|lync|skype.*for.*business|skype4b|sfb|s4b|zoom.*phone|3cx|aircall|dialpad|vonage|ooma|mitel|cisco.*spark|webex.*teams|ms.*teams)')
THEN TRUE
WHEN REGEXP_CONTAINS(LOWER(IFNULL(p2o_url_domain, '')),
r'(teams\.microsoft\.com|zoom\.us|skype\.com|webex\.com|meet\.google\.com|gotomeeting\.com|bluejeans\.com|hangouts\.google\.com)')
THEN TRUE
ELSE FALSE
END AS is_communication_app
FROM deduplicated_events
),
locked_detection AS (
SELECT
* EXCEPT(p2o_IsWorkstationLocked),
CASE
WHEN LOWER(IFNULL(p2o_app_exe, '')) LIKE '%winmainf%'
OR LOWER(IFNULL(p2o_app_exe, '')) LIKE '%netsendf%'
OR LOWER(IFNULL(p2o_app_exe, '')) LIKE '%pcf%'
OR LOWER(IFNULL(p2o_app_exe, '')) LIKE '%p2o%'
OR LOWER(IFNULL(p2o_app_exe, '')) LIKE '%push2do%'
OR LOWER(IFNULL(p2o_app_exe, '')) = '[system process]'
OR p2o_app_exe = 'No app info'
OR LOWER(IFNULL(p2o_app_exe, '')) LIKE 'lockapp%'
THEN TRUE
WHEN p2o_total_idle > 3000
AND SAFE_DIVIDE(p2o_total_idle, p2o_total_seconds) > 0.9
AND NOT is_communication_app
THEN TRUE
WHEN REGEXP_CONTAINS(LOWER(IFNULL(lag_p2o_url, '')), r'(screen saver)|(windows is being)')
AND REGEXP_CONTAINS(LOWER(IFNULL(lead_app_url, '')), r'(screen saver)|(windows is being)|(lockapp)')
THEN TRUE
WHEN p2o_title_windows = 'LockingWindow'
THEN TRUE
WHEN IFNULL(p2o_mouse_hooks, '') = ''
AND IFNULL(p2o_keyboard_hooks, '') = ''
AND NOT(lag_title_windows IS NULL AND lead_title_windows IS NULL)
AND (IFNULL(lag_title_windows, 'LockingWindow') = 'LockingWindow' OR cnt_prc_lockwindow > 0)
AND (IFNULL(lead_title_windows, 'LockingWindow') = 'LockingWindow' OR cnt_flw_lockwindow > 0)
THEN TRUE
ELSE IFNULL(p2o_IsWorkstationLocked, FALSE)
END AS p2o_IsWorkstationLocked
FROM communication_detection
),
employees_vacation AS (
SELECT DISTINCT
employee,
LAST_VALUE(vacation_date_in IGNORE NULLS) OVER (
PARTITION BY employee
ORDER BY creation_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS vacation_date_in,
LAST_VALUE(vacation_date_out IGNORE NULLS) OVER (
PARTITION BY employee
ORDER BY creation_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS vacation_date_out,
LAST_VALUE(vacation_active IGNORE NULLS) OVER (
PARTITION BY employee
ORDER BY creation_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS vacation_active
FROM `fnk-bi-empresa.EMPRESA.rel_hierarchy_employee_associative_EMPRESA`
WHERE vacation_date_in IS NOT NULL
AND vacation_active IS TRUE
),
events_no_vacation AS (
SELECT e.*
FROM locked_detection e
LEFT JOIN employees_vacation vac
ON LOWER(e.p2o_username) = LOWER(vac.employee)
AND e.p2o_datetime_in >= vac.vacation_date_in
AND e.p2o_datetime_out <= IFNULL(vac.vacation_date_out, TIMESTAMP('2099-12-31'))
WHERE vac.employee IS NULL
),
-- ===================================================================
-- JANELA DE JORNADA — porte do pipeline interno da Fhinck
--
-- A jornada e definida POR USUARIO a partir dos eventos DESBLOQUEADOS
-- com duracao > 0 (nao por usuario+maquina). Blocos contiguos sao unidos;
-- ha quebra de jornada quando o intervalo desde o ultimo desbloqueio
-- passa de 4h. A data do evento passa a ser a data de ENTRADA da jornada
-- (nao DATE(p2o_datetime_in)), e SOMENTE eventos dentro da janela
-- [entrada, saida] permanecem — tempo de maquina ligada fora da jornada
-- nao e atividade.
-- ===================================================================
unlocked_seq AS (
SELECT
p2o_username, p2o_datetime_in, p2o_datetime_out,
LAST_VALUE(p2o_datetime_out IGNORE NULLS) OVER (
PARTITION BY p2o_username
ORDER BY p2o_datetime_in, p2o_datetime_out
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
) AS last_unlocked
FROM events_no_vacation
WHERE p2o_IsWorkstationLocked = FALSE
AND p2o_total_seconds > 0
),
contiguous_blocks AS (
SELECT
p2o_username, p2o_datetime_in, p2o_datetime_out, last_unlocked,
SUM(IF(last_unlocked = p2o_datetime_in, 0, 1)) OVER (
PARTITION BY p2o_username
ORDER BY p2o_datetime_in, p2o_datetime_out
) AS gp_nr
FROM unlocked_seq
),
journey_blocks AS (
SELECT
p2o_username, gp_nr,
MIN(p2o_datetime_in) AS gp_entry,
MAX(p2o_datetime_out) AS gp_exit,
MIN(last_unlocked) AS gp_last_unlocked
FROM contiguous_blocks
GROUP BY p2o_username, gp_nr
),
journeys AS (
SELECT
p2o_username, gp_entry, gp_exit,
SUM(IF(TIMESTAMP_DIFF(gp_entry, gp_last_unlocked, SECOND) > 14400, 1, 0)) OVER (
PARTITION BY p2o_username
ORDER BY gp_entry, gp_exit
) AS journey_nr
FROM journey_blocks
),
journey_ranges AS (
SELECT
p2o_username,
MIN(gp_entry) AS journey_entry,
MAX(gp_exit) AS journey_exit
FROM journeys
GROUP BY p2o_username, journey_nr
),
journey_by_day AS (
SELECT
jr.p2o_username AS jw_user,
DATE(jr.journey_entry) AS jw_date,
MIN(jr.journey_entry) AS jw_entry,
MAX(jr.journey_exit) AS jw_exit
FROM journey_ranges jr, config c
WHERE DATE(jr.journey_entry) BETWEEN c.date_start AND c.date_end
GROUP BY jw_user, jw_date
),
entry_exit_calc AS (
SELECT
e.* EXCEPT(p2o_date),
w.jw_date AS p2o_date,
w.jw_entry AS p2o_entry,
w.jw_exit AS p2o_exit
FROM events_no_vacation e
JOIN journey_by_day w
ON e.p2o_username = w.jw_user
AND e.p2o_datetime_in >= w.jw_entry
AND e.p2o_datetime_out <= w.jw_exit
),
gap_detection AS (
SELECT
*,
LEAD(p2o_datetime_in) OVER (
PARTITION BY p2o_username, p2o_hostname, p2o_date
ORDER BY p2o_datetime_in, p2o_datetime_out
) AS next_event_start,
TIMESTAMP_DIFF(
LEAD(p2o_datetime_in) OVER (
PARTITION BY p2o_username, p2o_hostname, p2o_date
ORDER BY p2o_datetime_in, p2o_datetime_out
),
p2o_datetime_out,
SECOND
) AS gap_seconds
FROM entry_exit_calc
),
events_extended AS (
SELECT
* EXCEPT(p2o_datetime_out, p2o_total_seconds, p2o_total_idle, gap_seconds, next_event_start),
CASE
WHEN gap_seconds > 0
AND gap_seconds <= 14400
AND next_event_start IS NOT NULL
THEN next_event_start
ELSE p2o_datetime_out
END AS p2o_datetime_out,
CASE
WHEN gap_seconds > 0
AND gap_seconds <= 14400
AND next_event_start IS NOT NULL
THEN p2o_total_seconds + gap_seconds
ELSE p2o_total_seconds
END AS p2o_total_seconds,
CASE
WHEN gap_seconds > 0
AND gap_seconds <= 14400
AND next_event_start IS NOT NULL
THEN p2o_total_idle + (gap_seconds * SAFE_DIVIDE(p2o_total_idle, p2o_total_seconds))
ELSE p2o_total_idle
END AS p2o_total_idle
FROM gap_detection
),
classified_events AS (
SELECT
e.*,
`fnk-bi-empresa.EMPRESA.clean_textV2`(
e.p2o_title_windows,
'{"cleanTitle": true}',
tcr.clearing_array
) AS title_windows_clean,
`fnk-bi-empresa.EMPRESA.get_app_classification`(
acr.rules_array,
[
`fnk-bi-empresa.EMPRESA.clean_textV2`(e.p2o_title_windows, '{"cleanTitle": true}', tcr.clearing_array),
LEFT(IFNULL(e.p2o_url, ''), 1500),
IFNULL(e.p2o_url_domain, ''),
IFNULL(e.p2o_app_exe, ''),
IFNULL(e.p2o_app_exe_fullname, '')
],
IFNULL(e.p2o_url_domain, IFNULL(e.p2o_app_exe_fullname, e.p2o_app_exe))
) AS app_classification_result
FROM events_extended e
CROSS JOIN app_classification_rules acr
CROSS JOIN text_clearing_rules tcr
),
events_with_classification AS (
SELECT
* EXCEPT(app_classification_result, p2o_url_domain_corporate),
IFNULL(app_classification_result.app_name, IFNULL(p2o_url_domain, IFNULL(p2o_app_exe_fullname, p2o_app_exe))) AS app_name,
IFNULL(app_classification_result.app_class, 'other') AS app_class,
IFNULL(app_classification_result.app_class_id, 0) AS app_class_id,
CASE
WHEN p2o_app_exe IS NOT NULL
AND LOWER(p2o_app_exe) NOT IN (
'chrome.exe', 'firefox.exe', 'safari.exe', 'msedge.exe',
'iexplore.exe', 'opera.exe', 'brave.exe', 'chromium.exe',
'vivaldi.exe', 'waterfox.exe', 'seamonkey.exe', 'palemoon.exe',
'maxthon.exe', 'slimbrowser.exe', 'avant.exe', 'tor.exe',
'epic.exe', 'yandex.exe', 'edge.exe'
)
THEN 55
WHEN app_classification_result.app_class_id IS NOT NULL
AND app_classification_result.app_class_id > 0
THEN app_classification_result.app_class_id
WHEN p2o_url_domain IS NOT NULL
AND LOWER(p2o_url_domain) LIKE '%fhinck%'
THEN 1
WHEN p2o_url_domain IS NULL
OR TRIM(IFNULL(p2o_url_domain, '')) = ''
THEN 55
WHEN NOT REGEXP_CONTAINS(IFNULL(p2o_url_domain, ''),
r'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$')
THEN 55
WHEN REGEXP_CONTAINS(LOWER(IFNULL(p2o_url, '')), r'\.(pdf|jpg|png|doc|docx|xls|xlsx|xml)$')
THEN 55
ELSE 0
END AS p2o_url_domain_corporate
FROM classified_events
),
network_extracted AS (
SELECT
e.*,
cn.networks AS corporate_networks_list,
(
SELECT
LOWER(TRIM(SPLIT(network_part, '|')[SAFE_OFFSET(0)]))
FROM UNNEST(SPLIT(IFNULL(e.p2o_network, ''), ';')) AS network_part
WHERE ARRAY_LENGTH(SPLIT(network_part, '|')) >= 2
AND LOWER(TRIM(SPLIT(network_part, '|')[SAFE_OFFSET(1)])) = 'true'
AND LOWER(TRIM(SPLIT(network_part, '|')[SAFE_OFFSET(0)])) NOT IN (
'identificando', 'rede nao identificada', 'rede não identificada', '', 'null'
)
LIMIT 1
) AS detected_network
FROM events_with_classification e
CROSS JOIN corporate_networks cn
),
network_status AS (
SELECT
ne.*,
LAG(detected_network) OVER (
PARTITION BY p2o_username, p2o_hostname, p2o_date
ORDER BY p2o_datetime_in
) AS prev_network,
CASE
WHEN detected_network IS NOT NULL
AND detected_network IN UNNEST(corporate_networks_list)
THEN TRUE
ELSE FALSE
END AS is_current_corporate,
CASE
WHEN LAG(detected_network) OVER (
PARTITION BY p2o_username, p2o_hostname, p2o_date
ORDER BY p2o_datetime_in
) IS NOT NULL
AND LAG(detected_network) OVER (
PARTITION BY p2o_username, p2o_hostname, p2o_date
ORDER BY p2o_datetime_in
) IN UNNEST(corporate_networks_list)
THEN TRUE
ELSE FALSE
END AS is_prev_corporate
FROM network_extracted ne
),
initial_status AS (
SELECT
*,
CASE
WHEN is_current_corporate = TRUE
AND is_prev_corporate = FALSE
AND prev_network IS NOT NULL
THEN 'VPN'
WHEN is_current_corporate = TRUE
THEN 'Corp'
ELSE 'Outro'
END AS initial_network_status
FROM network_status
),
vpn_propagation AS (
SELECT
*,
MAX(CASE WHEN initial_network_status = 'VPN' THEN p2o_datetime_in END) OVER (
PARTITION BY p2o_username, p2o_hostname, p2o_date
ORDER BY p2o_datetime_in
ROWS UNBOUNDED PRECEDING
) AS vpn_start_time
FROM initial_status
),
network_final AS (
SELECT
*,
CASE
WHEN vpn_start_time IS NOT NULL
AND p2o_datetime_in >= vpn_start_time
AND is_current_corporate = TRUE
THEN 'VPN'
ELSE initial_network_status
END AS final_network_status
FROM vpn_propagation
),
work_mode_event AS (
SELECT
*,
CASE
WHEN final_network_status IN ('VPN', 'Outro') THEN 'Home-Office'
WHEN final_network_status = 'Corp' THEN 'Empresa'
ELSE 'Indeterminado'
END AS place_event
FROM network_final
),
events_with_block_count AS (
SELECT
*,
CAST(CEILING(p2o_total_seconds / 600.0) AS INT64) AS num_blocks,
p2o_total_seconds AS original_total_seconds,
p2o_total_idle AS original_total_idle
FROM work_mode_event
),
split_blocks AS (
SELECT
e.* EXCEPT(p2o_datetime_in, p2o_datetime_out, p2o_total_seconds, p2o_total_idle),
TIMESTAMP_ADD(e.p2o_datetime_in, INTERVAL (600 * block_idx) SECOND) AS p2o_datetime_in,
LEAST(
e.p2o_datetime_out,
TIMESTAMP_ADD(e.p2o_datetime_in, INTERVAL (600 * (block_idx + 1)) SECOND)
) AS p2o_datetime_out,
block_idx,
TIMESTAMP_DIFF(
LEAST(
e.p2o_datetime_out,
TIMESTAMP_ADD(e.p2o_datetime_in, INTERVAL (600 * (block_idx + 1)) SECOND)
),
TIMESTAMP_ADD(e.p2o_datetime_in, INTERVAL (600 * block_idx) SECOND),
SECOND
) AS p2o_total_seconds
FROM events_with_block_count e,
UNNEST(GENERATE_ARRAY(0, GREATEST(0, num_blocks - 1))) AS block_idx
WHERE TIMESTAMP_ADD(e.p2o_datetime_in, INTERVAL (600 * block_idx) SECOND) < e.p2o_datetime_out
),
events_split AS (
SELECT
* EXCEPT(original_total_seconds, original_total_idle, num_blocks, block_idx),
LEAST(
CAST(p2o_total_seconds AS FLOAT64),
GREATEST(
0.0,
original_total_idle * SAFE_DIVIDE(CAST(p2o_total_seconds AS FLOAT64), CAST(original_total_seconds AS FLOAT64))
)
) AS p2o_total_idle,
CASE
WHEN num_blocks > 1
THEN CONCAT(bigquery_id, '-', CAST(block_idx AS STRING))
ELSE bigquery_id
END AS new_bigquery_id
FROM split_blocks
),
place_day_calc AS (
SELECT
e.*,
SUM(CASE WHEN place_event = 'Home-Office' AND p2o_IsWorkstationLocked = FALSE
THEN p2o_total_seconds ELSE 0 END) OVER (
PARTITION BY p2o_username, p2o_date
) AS total_remote_seconds,
SUM(CASE WHEN place_event = 'Empresa' AND p2o_IsWorkstationLocked = FALSE
THEN p2o_total_seconds ELSE 0 END) OVER (
PARTITION BY p2o_username, p2o_date
) AS total_presential_seconds,
SUM(CASE WHEN p2o_IsWorkstationLocked = FALSE
THEN p2o_total_seconds ELSE 0 END) OVER (
PARTITION BY p2o_username, p2o_date
) AS total_active_seconds
FROM events_split e
),
events_with_place_day AS (
SELECT
*,
CASE
WHEN SAFE_DIVIDE(total_remote_seconds, total_active_seconds) > 0.10
AND SAFE_DIVIDE(total_presential_seconds, total_active_seconds) > 0.10
THEN 'Hibrida'
WHEN total_remote_seconds > total_presential_seconds
THEN 'Home-Office'
ELSE 'Empresa'
END AS place_day
FROM place_day_calc
),
-- ===================================================================
-- HIERARQUIA DO COLABORADOR
--
-- Duas fontes, com semanticas DIFERENTES — nao sao intercambiaveis:
--
-- dim_employee_daily -> nomes VIGENTES NA DATA (historico correto).
-- Uma linha por colaborador por dia. E daqui que saem employee_alias,
-- role_name, region_name, department_name e os niveis Department_1..10.
--
-- rel_hierarchy_employee_associative -> CODIGOS e PATHS da configuracao
-- ATUAL (o cadastro no data lake e um snapshot por login, sem
-- versionamento). Portanto fk_department_code, fk_role_code,
-- fk_region_code, fk_process_code, department_path e process_path
-- refletem o cadastro de HOJE, nao o da data do evento. Para analise
-- historica por departamento, use os campos de nome (dim), nao os codigos.
-- ===================================================================
employee_daily AS (
SELECT
employee,
p2o_date,
employee_alias,
role_name,
Region AS region_name,
department_name,
Department_1, Department_2, Department_3, Department_4, Department_5,
Department_6, Department_7, Department_8, Department_9, Department_10
FROM `fnk-bi-empresa.EMPRESA.dim_employee_daily_EMPRESA`
),
employee_registry AS (
SELECT * EXCEPT(rn)
FROM (
SELECT
LOWER(employee) AS employee_key,
fk_department_code,
fk_process_code,
fk_role_code,
fk_region_code,
department_path,
process_path,
active AS cadastro_ativo,
ROW_NUMBER() OVER (
PARTITION BY LOWER(employee)
ORDER BY search_date DESC, creation_date DESC
) AS rn
FROM `fnk-bi-empresa.EMPRESA.rel_hierarchy_employee_associative_EMPRESA`
)
WHERE rn = 1
)
SELECT
new_bigquery_id AS event_id,
p2o_username,
p2o_hostname,
-- hierarquia vigente na data (dim_employee_daily)
d.employee_alias,
d.department_name,
d.role_name,
d.region_name,
d.Department_1, d.Department_2, d.Department_3, d.Department_4, d.Department_5,
d.Department_6, d.Department_7, d.Department_8, d.Department_9, d.Department_10,
-- codigos e paths do cadastro atual (rel_hierarchy_employee_associative)
r.fk_department_code,
r.fk_role_code,
r.fk_region_code,
r.fk_process_code,
r.department_path,
r.process_path,
e.p2o_date,
FORMAT_DATE('%Y-%m', e.p2o_date) AS mes,
CASE EXTRACT(DAYOFWEEK FROM e.p2o_date)
WHEN 1 THEN 'Domingo'
WHEN 2 THEN 'Segunda-feira'
WHEN 3 THEN 'Terça-feira'
WHEN 4 THEN 'Quarta-feira'
WHEN 5 THEN 'Quinta-feira'
WHEN 6 THEN 'Sexta-feira'
WHEN 7 THEN 'Sábado'
END AS dia_semana,
p2o_datetime_in,
p2o_datetime_out,
p2o_entry,
p2o_exit,
app_name,
app_class,
app_class_id,
title_windows_clean,
p2o_url_domain,
p2o_app_exe,
p2o_app_exe_fullname,
p2o_IsWorkstationLocked,
p2o_url_domain_corporate,
place_day,
CAST(p2o_total_seconds AS INT64) AS p2o_total_seconds,
CAST(p2o_total_idle AS INT64) AS p2o_total_idle,
CASE
WHEN REGEXP_CONTAINS(
IF(p2o_IsWorkstationLocked, 'OutPC',
IF(p2o_url_domain_corporate < 55, p2o_url_domain,
IFNULL(NULLIF(p2o_app_exe_fullname, ''), LOWER(p2o_app_exe)))),
r'(?i)excel|powerpoint|word|docs.google|analytics.google')
THEN 'Analítico'
WHEN IF(p2o_IsWorkstationLocked, 'OutPC',
IF(p2o_url_domain_corporate < 55, p2o_url_domain,
IFNULL(NULLIF(p2o_app_exe_fullname, ''), LOWER(p2o_app_exe)))) = 'OutPC'
THEN 'Relacionamento'
WHEN REGEXP_CONTAINS(
IF(p2o_IsWorkstationLocked, 'OutPC',
IF(p2o_url_domain_corporate < 55, p2o_url_domain,
IFNULL(NULLIF(p2o_app_exe_fullname, ''), LOWER(p2o_app_exe)))),
r'(?i)whatsapp|mail|outlook|chat|hangout|teams|jabber|skype|zoom|meet|telegram|whereby|discord|bluejeans|webex')
THEN 'Comunicação'
WHEN REGEXP_CONTAINS(
IF(p2o_IsWorkstationLocked, 'OutPC',
IF(p2o_url_domain_corporate < 55, p2o_url_domain,
IFNULL(NULLIF(p2o_app_exe_fullname, ''), LOWER(p2o_app_exe)))),
r'(?i)rstudio|datastudio|qlik|tableau|embarcadero|power bi|looker|visual studio|jupyter|colab.research|notepad\+\+|atom|github|comandos|command|sourcetree|powershell|developers|cloud.google|aws|localhost')
OR REGEXP_CONTAINS(IFNULL(p2o_title_windows, ''), r'(?i)power bi|jupyter')
THEN 'Data e Desenvolvimento'
ELSE 'Transacional'
END AS profile_name_event
FROM events_with_place_day e
LEFT JOIN employee_daily d
ON LOWER(e.p2o_username) = d.employee
AND e.p2o_date = d.p2o_date
LEFT JOIN employee_registry r
ON LOWER(e.p2o_username) = r.employee_key
