CORTEX — tracking-service (berço do lead e comportamento)
Versão: 0.10.0 · Data: 2026-10-14 · Status: Rascunho · Owner: Plataforma Caminho canônico:
cortex-docs/services/TRACKING_SERVICE.mdDocumentos irmãos:cortex-docs/ECOSYSTEM.md(mapa e fronteiras) ·cortex-docs/services/IDENTITY_SERVICE.md(identidade e PII) ·cortex-docs/standards/UNISCALE_ENGINEERING_STANDARDS.md(padrões técnicos)
Este documento promove a regras endereçáveis as decisões do tracking-service, cujo modelo de propriedade já estava fechado no ECOSYSTEM.md §3/§4/§5 mas cujo desenho estava explicitamente adiado em §7 ("Schemas de tracking e identity").
O tracking-service é o berço do lead_id, o dono do comportamento pseudônimo e a borda de ingestão exposta do CORTEX. Ele não sabe quem a pessoa é (isso é do identity-service) e não decide regra de negócio (isso é do institutional-service) — é executor.
Leia junto com o
IDENTITY_SERVICE.md. Os dois serviços são codependentes: otrackingcunha olead_id(navegador), oidentitycunha operson_id(pessoa). Cada regra de fronteira aqui tem a recíproca lá.
1. Berço do lead_id: um identificador de navegador, não de pessoa (Obrigatório)
O lead_id é cunhado num lugar só: o tracking-service, na primeira ação monitorada de qualquer origem, inclusive anônima (ECOSYSTEM §3).
- O
lead_idé um UUIDv7 gerado pela função oficial da plataforma (uniscale.columns.uuid-generation) e é oidda tabelatracking.leads. - O
lead_ididentifica um navegador/origem de navegação, não uma pessoa. Quem identifica pessoa é operson_id, cunhado peloidentity-serviceapenas quando existe PII (ecosystem.services.identity-person-mint). Confundir os dois é o erro que este parágrafo existe para impedir. - Nenhum outro serviço cunha
lead_id. Serviço que observa uma origem nova publica um fato; quem cunha é o tracking (§4). - O
lead_idnão circula pelo ecossistema como chave de pessoa: fora dotrackinge doidentity, os serviços falamperson_id(§13).
2. Comportamento é dele; PII não (Obrigatório)
Divisão de propriedade com o identity-service, sem sobreposição (ECOSYSTEM §5 — regra anti-redundância):
Fica no tracking |
Fica no identity |
|---|---|
| Trilha de navegação, tráfego/gtag, alto volume | Nome, email, telefone, documento |
| Contexto por evento: sessão, ip, geo, device, utm, referrer | Matching, person_id e fusão de pessoas |
Tudo chaveado por lead_id, pseudônimo |
"Quem é" e "por onde passou" |
- O
trackingnão armazena PII em repouso, com a única exceção do inbox de captura (§6), que é cifrado e efêmero por construção. - O
trackingnão responde "quem é essa pessoa". Essa pergunta é síncrona aoidentity. - O contexto cru de evento não é duplicado no
identity.
3. Borda exposta à internet: relaxe consciente (Obrigatório)
O tracking-service é a borda de ingestão exposta — não existe capture-service à parte (ECOSYSTEM §4).
- Ficam expostos à internet pública: o endpoint de coleta de eventos (beacon do SDK de browser) e o endpoint de recepção de formulário de terceiro.
- A exposição é um relaxe consciente e delimitado do princípio "dono de dado sensível nunca exposto": vale só porque comportamento é pseudônimo. PII segue para o
identitymurado (§7), que não fica exposto. - A API de leitura do comportamento (§14) é interna, nunca exposta à internet.
- Forma de infra da frente (ingress, WAF, rate-limit) é do plano de infra, não deste documento.
4. Nascimento fora da web usa o mesmo berço (Obrigatório)
Lead que chega por canal não-web (WhatsApp, importação, connector) nunca passou por LP e não tem cookie. O berço continua sendo um só.
- O connector de canal publica um fato de contato; o
trackingconsome e cunha olead_idnormalmente, combirth_sourceregistrando a origem (web,whatsapp,import, …). - A chave de matching desse nascimento é derivada do identificador do canal hasheado (ex.: telefone), nunca o valor em claro.
- O
trackingnão guarda o identificador em claro: ele entra na fatia de PII e segue para oidentitypor §6/§7.
5. Consentimento em dois níveis, viajando junto com o dado (Obrigatório)
O ecossistema opera com LPs com banner de consentimento, LPs sem banner, e com a possibilidade de recusa. O serviço suporta os três sem desenho alternativo.
O cookie first-party cortex_lid (HttpOnly, Secure, SameSite=Lax, TTL 400 dias) carrega apenas o lead_id e é gravado nos três casos — é identificador próprio, pseudônimo, de uso exclusivo do CORTEX no próprio site. O que o consentimento governa não é o cookie, é o uso do dado:
Base (absent ou denied) |
Consentido (granted) |
|
|---|---|---|
Cunhagem do lead_id e captura de comportamento |
sim | sim |
| IP | truncado (/24 IPv4, /48 IPv6) |
completo |
| Geo | até cidade | completo |
| Fingerprint de device | proibido | proibido |
Envio de conversão ou de pseudônimo de correlação a terceiro (marketing-connector → meta/google; analytics de página, §20) |
proibido | permitido |
| Retenção do cru | 6 meses | 24 meses (§12) |
tracking.sessionsregistraconsent_state(granted,denied,absent),consent_sourceeconsent_at. Vocabulário fechado (uniscale.service-internals.state-machine).- Todo fato publicado carrega o
consent_state. O consumidor decide o que pode fazer sem consultar ninguém; consumidor que ignora está em violação. - Fingerprint de device (canvas, fontes, entropia de browser) é proibido nos dois níveis — não há base legal limpa e a doc restringe o nascimento a "sinais permitidos por LGPD" (ECOSYSTEM §5).
- A validação da base legal é do jurídico/DPO. Esta regra fixa a mecânica, não o parecer.
6. Captura durável: persistir antes de ackar (Obrigatório)
Formaliza o mecanismo do ECOSYSTEM §5, fixando os parâmetros que lá ficaram em aberto. O formulário da LP é de terceiro: o CORTEX não controla o retry dele, então o instante do recebimento é a única chance que existe.
Sequência obrigatória ao receber uma submissão:
- Cunha (ou reconhece) o
lead_id(§1) e grava o comportamento. - Cifra a fatia de PII e grava em
tracking.capture_inboxcomstatus = 'pending'. - Só então responde 200 ao formulário.
- Um worker in-process (dentro do próprio serviço — sem pod novo) drena para o
identitypelo canal de §7. - Confirmada a entrega:
status = 'delivered'e opayload_encryptedé apagado imediatamente, não em lote diário. A linha permanece comlead_id+ timestamps como prova de auditoria.
Parâmetros:
- Backoff exponencial de 1s a 5min, teto de tentativas de 24h.
- Esgotado o teto:
status = 'dlq_retained'— retido para inspeção humana, nunca descartado. "N tentativas e dropa pro log" é proibido: perde o lead. - Cifra AES-256-GCM com chave de envelope; rotação de chave registrada.
- Consequência aceita e forçada pelo formulário de terceiro: PII fica em repouso cifrada por segundos, mitigada por TTL curto e apagamento após confirmação.
7. Entrega de PII ao identity é canal dedicado, não topic (Obrigatório)
O fato que carrega a fatia de PII (tracking.lead.identified) não é publicado no exchange topic do domínio.
- Vai em exchange
directdedicado (tracking-pii), com uma única queue declarada (identity-ingestion__lead-identified) e nenhum outro bind permitido. DLQ obrigatória (uniscale.service-internals.messaging-naming). - Payload cifrado em trânsito e em repouso na fila.
- O
identitycontinua sendo escrito só por fila (ECOSYSTEM §4) — esta regra não abre escrita direta.
Justificativa do desvio (registrar como exceção por uniscale.governance.exception-process): o exchange topic existe para que o publicador não saiba quem ouve — propriedade correta para fato de negócio, e exatamente o defeito para PII.
- Um bind novo em
tracking.#dá a quem o criou uma cópia de toda PII da borda, sem uma linha de código alterada, sem code review, sem alerta. - Com topic, "quem tem cópia da PII" é estado de runtime do broker, não artefato revisável — resposta insustentável numa auditoria LGPD.
- Contradiz o princípio declarado em ECOSYSTEM §4/§7: o relaxamento de exposição vale só para comportamento pseudônimo; a PII vai para o
identitymurado.
Com exchange direct, ler PII exige alterar infraestrutura declarada — o que passa por revisão. O acoplamento resultante (o tracking conhece um destino chamado identity) é intencional: são serviços codependentes por natureza.
8. Identificação do tenant e da propriedade é resolvida no servidor (Obrigatório)
- O
institutional-servicecadastra a propriedade digital (site, LP, formulário, conteúdo) e a quem ela pertence, e emite umawrite_keypor propriedade — não por tenant. A config é distribuída como fato versionado (ecosystem.services.institutional-config-distribution); otrackingmantém cópia local — se o institutional cair, a captura continua. - A
write_keyé pública por natureza (está no HTML da página): ela identifica, não autentica. tenant_idedigital_property_idsão resolvidos dawrite_key, no servidor. Valor de tenant ou de propriedade que venha no payload do cliente é ignorado — aceitá-lo permitiria escrever na conta de qualquer tenant/propriedade.- O
source_refcontinua no evento, mas não é o mecanismo de atribuição: é sub-referência dentro da propriedade (qual formulário da página), declarado pelo cliente e, como o tenant, sem valer como identidade. Quem a página é vem da chave; o que dentro dela, dosource_ref. allowed_origins(CORS) é a origem daquela propriedade, não a união dos domínios do tenant — e rate-limit no ingress completa a proteção.tenant_idestá em toda tabela de negócio desde o dia 1 (uniscale.columns.multi-tenancy).
Por que a chave é por propriedade, não por tenant. (a) Atribuição verificada, não declarada — a tag copiada de uma LP para outra sem trocar o source_ref atribuiria à página errada para sempre, sem nada acusar; a chave resolvida no servidor não mente. (b) Allowlist deixa de ser frouxa — por tenant, ela lista os domínios de todas as LPs, e um script comprometido numa delas postaria como qualquer outra. (c) Raio de revogação contido — chave por tenant significa que revogar uma vazada derruba todas as LPs do tenant de uma vez; com dezenas de LPs em plataformas distintas, revogação atômica não existe e, na prática, ninguém revoga. Não é trabalho novo para o institutional: ele já cadastra a propriedade (ecosystem.services.institutional-tenant-registry) — emitir a chave junto é consequência.
8-bis. Rotação da write_key tem critério de parada (Obrigatório)
Chave pública que nunca é trocada só some quando vaza. Rotação exige duas chaves válidas ao mesmo tempo — não se troca a tag de dezenas de LPs em WordPress, GreatPages e Webflow no mesmo instante.
- A
ingest_keyganhavalid_until(nullable). A chave em retirada continua aceita até esse instante declarado — em vez de umis_activeque alguém desliga no escuro. - Sequência: emitir a nova → publicar nas páginas → só então marcar a antiga com
valid_until. - A parte difícil não é emitir; é saber quando a velha parou de ser usada. Por isso a regra exige telemetria de uso da chave:
sessions.ingest_key_id— na sessão, não no evento (a sessão nasce uma vez por visitante; no evento multiplicaria escrita sem responder mais nada). - Não se revoga chave sem consultar essa telemetria. É o que transforma rotação de mecanismo em procedimento: sem a resposta, revogar é aposta — ou se espera "tempo suficiente" e uma LP esquecida cai, ou não se revoga nunca e a rotação não termina.
9. Idempotência da ingestão pela chave do cliente (Obrigatório)
- O SDK gera um
client_event_id(UUID) no navegador, uma vez por evento, e reenvia sempre o mesmo em caso de retry de rede. - No banco: índice único
ux_events__tenant_id__client_event_id(uniscale.constraints.prefixes). Reentrega bate no índice e é descartada em silêncio, com 200. - Sem isso, oscilação de rede duplica
page_viewe o funil vira ficção; e duplo clique em submit gera doislead_idpara a mesma origem. - Alinhado a
uniscale.lineage.idempotency: re-entrega não duplica dado.
10. Vocabulário de evento é controlado e extensível sem deploy (Obrigatório)
tracking.events.event_typeévarcharvalidado contra a tabela lookuptracking.event_types— nuncaenumde Postgres e nunca string livre.- Criar um tipo novo é um
INSERT, não umALTER TYPE: sem migração, sem deploy coordenado. String livre foi rejeitada pelo motivo oposto — vira lixo não-consultável em poucos meses. tracking.event_types.tenant_idé nullable, e a coluna é a fronteira de propriedade do catálogo — o critério é quem é dono do significado:tenant_id IS NULL(global) → dono é otracking, entra por migration, versionado com o código. Um tipo global só existe se o SDK souber emiti-lo (page_view,click,form_submit): é contrato, não config — acrescentar exige emissor e consumidores entenderem, por isso nasce com o código.tenant_idpreenchido → dono é oinstitutional, entra por fato de configuração (ecosystem.services.institutional-config-distribution), réplica local.cortex.track('visita_ao_campus')não exige nada nosso: é vocabulário de negócio, e o dono do negócio é oinstitutional.
- A mesma fronteira vale, de forma idêntica, para os três catálogos:
event_types(§10),acquisition_param_types(§10-bis) ebusiness_metadata_keys(§21) — todos comtenant_idnullable, global = contrato dotracking, por-tenant = config doinstitutional. Tratá-los diferente é o começo da confusão, e os três já têm exatamente a mesma forma. - Terceira origem do vocabulário: derivada de integração. Além do que entra pela borda de captura (o SDK emite:
page_view,click,form_submit) e do vocabulário de negócio de um tenant (config doinstitutional), há uma categoria própria: o evento que nasce dentro do serviço como consequência de consumir um fato de outro serviço — ninguém de fora o emite, ele não tem contrato de borda e não pode chegar pela ingestão pública. Ocrm_stage_change(§26) é o primeiro: a mudança de etapa do card no CRM, capturada pelocrm-connectore consumida como fato, vira um marco na trilha do lead. Seu significado é contrato de plataforma (dono é otracking, como os globais), mas sua via de entrada é o consumo de fato, não a borda. Reconhecer essa origem à parte evita dois erros simétricos: tratá-la como config de tenant (não é — o sentido é da plataforma) e esperá-la na borda pública (nunca chega por lá; recusá-la ali é o correto). - Sementes do catálogo global:
session_start,page_view,click,form_view,form_submit,doc_read,video_progress. - O que não couber em coluna própria vai em
metadata jsonb(uniscale.columns.optional) — nunca dado que merece coluna.
10-bis. Parâmetro de aquisição é dinâmico, e o catálogo é datado (Obrigatório)
Os UTMs são coluna porque são conjunto fechado — source/medium/campaign/term/content/id (seis campos: utm_id entra porque é a chave de reconciliação de campanha no Meta/GA4, tão estável e tão padrão quanto utm_campaign). O que chega sem aviso é outra coisa: gclid, fbclid, msclkid, ttclid, ?parceiro=X, ?convenio=Y.
- Tudo que não é um desses seis UTMs vai para
sessions.acquisition_params(jsonb, índice GIN) — inclusive umutm_*fora do conjunto fechado (ex.:utm_source_platform): é parâmetro de aquisição como qualquer outro e não pode cair no vão entre a regra dos UTM ("pula o que começa comutm_") e a regra do resto, nem sumir em silêncio. Parâmetro novo não exige DDL: nem coluna, nem migração, nem deploy coordenado. O que exige DDL é a otimização, e só quando o parâmetro provar que merece:CREATE INDEX ... ((acquisition_params->>'gclid'))é B-tree sobre o valor extraído, com a mesma performance de coluna dedicada, sem mover dado. tracking.acquisition_param_typesregistrafirst_seen_atautomaticamente, na primeira vez que o parâmetro aparece. Ele existe pela semântica, não pelo armazenamento.- O teto é por destino, e estourar não vira
500. Coluna UTM évarchar(255);acquisition_params(jsonb) não tem largura fixa e comporta teto maior (umfbclidreal passa de 200 caracteres). Valor acima do limite do seu destino é recusado na borda com422nomeando o campo (§22) — nunca deixado estourar noINSERTe virar500, que o SDK reenfileira para sempre. Um teto único para destinos de larguras diferentes é o próprio defeito.
Por que o catálogo é obrigatório. Consultar em 2028 "sessões sem gclid" mistura duas coisas que não são a mesma: a visita que não veio daquele canal (ausência conhecida) e a visita anterior à captura do parâmetro (desconhecido). Tratar as duas como "não veio" produz a conclusão errada — "o canal não trouxe ninguém em 2026" — quando só não estávamos etiquetando. A consulta correta tem três respostas, e se limita à janela em que a resposta existe.
Isso não é peculiaridade de JSONB: coluna acrescentada em 2027 deixa as linhas de 2026 com NULL, igualmente indistinguível, e banco de documentos idem — schemaless não responde semântica, apenas esconde a pergunta. A resposta é o catálogo datado, qualquer que seja o formato de armazenamento.
Tetos de construção (a borda é pública). POST /collect é endpoint público e não autenticado; um cliente fora do nosso SDK — ou o SDK com o JavaScript alterado, que qualquer pessoa consegue fazer — pode mandar mil parâmetros de dez mil caracteres, que entram no jsonb e viram linhas novas em acquisition_param_types (o cadastro é automático). É a mesma exposição que o §25 trata na fila de descoberta, e a mesma resposta: teto de parâmetros distintos na sessão (o jsonb), teto de nomes distintos por tenant (o catálogo), teto de tamanho de nome e de valor, e vocabulário de caracteres restrito no nome — validados no servidor, não só no SDK (o teto do SDK, MAX_PARAMS/MAX_PARAM_LENGTH, é conveniência, não controle). Ao atingir o teto de nomes distintos, o auto-cadastro para de registrar nomes novos e só conta — o jsonb ainda guarda o que veio na sessão, mas a tabela-catálogo não cresce a partir da internet. Os três limites têm de existir; os valores vivem no template/config, não nesta regra (precedente do §30.7 e do §25), e o serviço registra os limites efetivos no arranque — afrouxar teto não pode ser silencioso.
Click id é dado pessoal, e o controle é o apagamento — não a retenção curta. gclid, fbclid, gbraid, wbraid, msclkid, ttclid são identificadores de clique: a plataforma que os emitiu liga cada um a uma pessoa identificada, então são dado pessoal de baixa sensibilidade (identificável por terceiro, ainda que não por nós — o teste da LGPD é identificável, não secreto). O tratamento é deliberado, e cada linha fecha uma alternativa que foi pesada e descartada:
- Retidos junto da sessão, para atribuição de campanha — inclusive de longo prazo (LTV): sem o click id, a definição de campanha guardada no banco não reconstrói qual clique foi daquela pessoa.
- Não cifrados — são valores públicos (viajam na URL, ficam no navegador da própria pessoa); cifrar
acquisition_paramsderrubaria a consulta analítica que é a razão de ele existir, sem proteger contra o que importa. - Apagados pelo esquecimento do titular, automaticamente: o
identity.person.erasedjá purga as sessões de todos oslead_idda pessoa (§12), e os click ids vão junto pela cadeia pessoa →lead_id→session. Este é o controle — o direito ao esquecimento os alcança sem regra nova. - Sem expiração própria, porque expirar não protegeria contra ameaça realista: o click id só re-identifica com acesso à conta/pixel da plataforma emissora — quem tem esse acesso não precisa do nosso banco, e quem tem só o nosso banco não resolve o id sozinho. Expirar custaria a atribuição e não mitigaria risco.
11. Modelo de dados (Obrigatório)
Banco próprio tracking_db, schema tracking (uniscale.naming.databases). Sem banco vetorial — o tracking não toca vetorização.
| Tabela | Papel | Soft delete |
|---|---|---|
tracking.leads |
O berço. id = lead_id, matching_key_hash, first_seen_at, last_seen_at, birth_source |
sim |
tracking.sessions |
Sessão e contexto: utm_*, acquisition_params, referrer, ip_inet, geo, device, consent_state |
não |
tracking.events |
Comportamento. event_type, occurred_at, url, metadata. Particionada por mês |
não |
tracking.capture_inbox |
Fatia de PII cifrada em trânsito para o identity (§6) | sim |
tracking.lead_identities |
Vínculo lead_id ↔ person_id (§15) |
não |
tracking.lead_identity_journal |
Diário append-only de vínculos e refusões, para auditoria | não |
tracking.event_types |
Catálogo de tipos (§10) | sim |
tracking.acquisition_param_types |
Catálogo datado dos parâmetros de aquisição (§10-bis) | sim |
tracking.business_metadata_keys |
Catálogo das chaves de negócio do formulário, não-PII (§21) | sim |
tracking.metadata_key_candidates |
Fila de descoberta: nomes de chave recusados, para revisão — nunca o valor (§25) | não |
tracking.ingest_keys |
Réplica local da config do institutional: write_key, digital_property_id, allowlist de origem, config_version, valid_until (§8, §8-bis) |
sim |
tracking.outbox |
Outbox transacional dos fatos publicados | não |
- Colunas base obrigatórias em toda tabela (
uniscale.columns.base);tenant_idem toda tabela de negócio (§8). person_idemtracking.lead_identitiesé referência externa aoidentity-service: sem FK local, comCOMMENT ON COLUMNobrigatório declarando a origem (uniscale.constraints.foreign-keys— FK não cruza fronteira de serviço).- A dispensa de soft delete em
sessions,events,lead_identities,lead_identity_journal,outboxemetadata_key_candidatesé decisão explícita exigida poruniscale.columns.soft-delete: são evento técnico de alto volume, registros de linhagem sem valor de auditoria de vínculo, ou fila transitória que expira (a de descoberta, §25). O direito ao esquecimento se cumpre por purga física (§12), não por soft delete. - A dispensa de
created_by/updated_bynas mesmas tabelas técnicas é decisão explícita, pelo mesmo critério: emeventsseriam dois UUID constantes por linha numa tabela que cresce em bilhões, apontando sempre para a única conta de serviço do ingestor — custo real, informação zero. Vale parasessions,events,lead_identities,lead_identity_journaleoutbox; as de negócio (leads,capture_inbox,event_types,acquisition_param_types,business_metadata_keys,ingest_keys) mantêm autoria integral (uniscale.columns.authorship). - Índices únicos em tabela com soft delete são parciais (
WHERE deleted_at IS NULL).
12. Retenção, particionamento e purga (Obrigatório)
tracking.eventsé particionada por mês; expurgo por drop de partição, nuncaDELETEem massa.- Retenção do comportamento cru: 24 meses no nível consentido, 6 meses no nível base (§5).
- Purga por titular: o
trackingconsomeidentity.person.erased(ecosystem.services.identity-erasure) e apaga fisicamente comportamento, sessões, leads e vínculos de todos oslead_iddaquela pessoa, registrando a execução. Otrackingnão decide apagamento — obedece ao fato. - Log operacional vai para o Mongo com TTL e sem PII (
uniscale.mongo.forbidden-content) — log referencia por id, não copia registro.
13. Fatos publicados, e o tracking não fala com terceiros (Obrigatório)
Exchange tracking, tipo topic; envelope e nomenclatura padrão (uniscale.events.envelope, uniscale.events.naming).
| Fato | Quando | Canal | Consumidores |
|---|---|---|---|
tracking.lead.created |
1ª ação monitorada, sempre | topic tracking |
identity (registra o lead anônimo), crm-connector (cria card), cortex-service |
tracking.lead.identified |
chegou fatia de PII | direct tracking-pii (§7) |
só identity |
tracking.event.captured |
marcos de comportamento (§14) | topic tracking |
cortex-service, marketing-connector (respeitando §5) |
- Todo fato carrega o
person_idquando ele já é conhecido, e sempre olead_id. Serviços fora do par tracking/identity usam operson_id; olead_idé contexto de origem, não chave de pessoa (§1). - O
trackingnunca chama a API do Bitrix (nem de qualquer terceiro): publica o fato, e ocrm-connectorcria/move o card aplicando o mapa configurado noinstitutional. Mesma fronteira deecosystem.services.ai-wallet-crm-refs. - O
trackingnão decide raia de CRM — aplica-se o princípio de executor (ECOSYSTEM §2). - Publicação via outbox transacional: o fato e a escrita local commitam juntos.
14. O core recebe marco e ponteiro; o cru fica na fonte (Obrigatório)
- Vão ao
cortex-serviceos marcos (lead.created,lead.identified,form_submit,doc_read) e o ponteiro.page_vieweclickcrus não são publicados — inflariam o core sem ganho analítico proporcional. - O comportamento cru permanece no
tracking_dbe é consultado na fonte (ECOSYSTEM §5: "contexto rico é puxado na fonte"; o core consolida para análise, não para servir contexto ao vivo). - Consequência obrigatória: o
trackingexpõe uma API de leitura interna (síncrona, não exposta à internet — §3) que permite drillar comportamento porperson_ide porlead_id, com recorte de período. Sem ela o ponteiro no core aponta para o nada e o dashboard não se constrói.
15. Vínculo lead_id ↔ person_id: o tracking grava, não decide (Obrigatório)
Quem cunha person_id e quem decide que dois registros são a mesma pessoa é o identity-service (ecosystem.services.identity-person-mint, ecosystem.services.identity-matching-signals). O tracking cunha lead_id e obedece — nunca decide identidade.
- O
trackingconsomeidentity.person.identifiede grava uma linha emtracking.lead_identities(lead_id,person_id,bound_at,bound_session_id). O consumo é do exchange topicidentity, pela filatracking-identity__person-facts(uniscale.service-internals.messaging-naming), com ack manual, DLQ e idempotência pelo índice único desource_event_idno diário. - O
trackingnão reescrevetracking.events. O evento cru é imutável: registra o que foi observado, não o que depois se soube. O vínculo é uma camada separada, resolvida na leitura (§17).
Justificativa da imutabilidade (reescrita foi rejeitada por três motivos):
- Os outros não são reescrevíveis. No instante do vínculo, o
lead_idjá foi ao core, já virou card no Bitrix e já foi enviado como conversão à Meta. Reescrever só o tracking o dessincroniza de todos. - Identificação erra. Com vínculo em tabela à parte, desfazer é remover uma linha; com reescrita, o original não existe mais.
- Volume.
UPDATEde milhões de linhas atravessando partições, disparado por consumo de fila, é incidente de produção.
Cardinalidade (Obrigatório): o esperado é 1 person_id para N lead_id (a mesma pessoa se identificando no celular, no desktop e no computador do trabalho). O caso inverso — N pessoas num mesmo lead_id, computador compartilhado por uma família — é exceção reconhecida e não modelada como regra: a tabela permite mais de um vínculo para o mesmo lead_id, o comportamento daquele navegador fica impreciso por pessoa, e a operação segue. bound_at e bound_session_id ficam registrados apenas para forense, não são regra de atribuição.
Fusão de pessoas (Obrigatório): quando o identity funde dois person_id, publica identity.person.merged; o tracking reaponta os vínculos do person_id absorvido para o sobrevivente, na mesma transação, e registra no diário append-only. Consequência: o estado final independe da ordem de chegada dos fatos (uniscale.events.ordering — entrega at-least-once e fora de ordem) e reprocessar a fila inteira dá o mesmo resultado. Idempotente por construção, não por deduplicação. identity.person.unmerged é aplicado da mesma forma, em sentido inverso.
O cookie nunca é reescrito. Um navegador é aquele lead_id até o cookie expirar (400 dias, §5) — é isso que permite reconhecer que a pessoa já esteve nas páginas duas semanas atrás. Vínculo de identidade não altera o lead_id.
16. Vínculo e fusão nunca atravessam tenant (Obrigatório)
- Constraint dura
ck_lead_identities__same_tenant: vínculo só entrelead_ideperson_iddo mesmotenant_id. - Fato de vínculo ou fusão que atravesse tenant não é aplicado: vai para DLQ retida e dispara alerta.
- Não existe cenário legítimo cross-tenant. Se esta validação falhar, o vazamento é entre clientes — é a falha mais grave possível neste serviço.
17. Leitura resolve a pessoa (Obrigatório)
- Consulta por
person_iddevolve o comportamento de todos oslead_idvinculados àquela pessoa — não o de um navegador só. Consultar por qualquerlead_idvinculado e consultar peloperson_idsão recortes diferentes e ambos legítimos; o que não pode acontecer é a mesma pergunta dar respostas diferentes. - A resolução é encapsulada em view ou na camada de acesso, como o filtro de
deleted_at(uniscale.columns.soft-delete). - Comportamento de
lead_idsem vínculo é consultável porlead_ide invisível porperson_id— permanece pseudônimo até que exista pessoa. - Comportamento de
lead_idmarcadosharedé igualmente invisível porperson_id, mesmo havendo vínculo — ver §18. - Escrita: o comportamento continua chegando com o
lead_iddo cookie e é gravado como observado. Operson_idé resolvido no momento de publicar o fato (§13), não no de gravar.
15-bis. (Substituída) Fusão de leads: apelido, nunca reescrita de histórico
Regra da v0.1.0, substituída na v0.2.0 por ecosystem.services.tracking-lead-identities.
Ela descrevia a fusão como escolha de um lead_id canônico entre vários, com o tracking mantendo tracking.lead_aliases. Esse modelo foi abandonado quando o person_id passou a ser cunhado pelo identity na identificação (ecosystem.services.identity-person-mint): não existe lead_id vencedor — os lead_id coexistem, todos vinculados ao mesmo person_id, e a fusão acontece entre pessoas, dentro do identity.
Mantida aqui apenas como slug depreciado, conforme doc.slug.stability (slug publicado é imutável e nunca reusado). Não implementar.
18. Navegador compartilhado: o lead admite não saber (Obrigatório)
Três pessoas usando o mesmo computador produzem um lead_id. Enquanto ninguém se identifica, não existe informação no mundo que diga qual das três está navegando — nenhum algoritmo resolve isso, e fingerprint de device é proibido (§5) e não separaria mesmo.
O erro a evitar não é o split: é atribuir. Se o comportamento daquele navegador for herdado por todas as pessoas vinculadas, o ai-sales conduz a conversa do candidato A com o interesse que era da irmã, e a ficha de A passa a exibir o que C navegou — vazamento entre duas pessoas que só dividem um teclado.
tracking.leads.sharing_state, vocabulário fechado:single|shared. Nascesingle.- Transição por detecção, nunca por configuração: ao vincular um segundo
person_iddistinto ao mesmolead_id, o lead virashared. Não volta parasingleautomaticamente. - Em
shared, a consulta por pessoa não devolve comportamento nenhum daquele navegador. O dado não some: continua consultável porlead_id. O que muda é o serviço passar a responder "não sei de quem foi" em vez de chutar. - O vínculo N:N continua sendo gravado (§15). Ele é fato — "estas pessoas usaram este navegador" — e serve para auditoria e fraude. O que fica proibido é a leitura atravessar esse vínculo para herdar comportamento.
- O fato da identificação (quem se identificou, e quando) não se perde: vive no
identity(person_leads.bound_at). O que fica sem dono é a linha crua do evento.
Consequência aceita: em computador compartilhado, a trilha por pessoa acaba. Ela nunca existiu — o que existia era invenção, e invenção que ia para o CRM, para o core e para a IA como se fosse verdade. Decisão errada custa mais que decisão não tomada.
19. Comportamento de página é da ferramenta externa; o da pessoa é nosso (Obrigatório)
A fronteira não é por tipo de dado, é por sujeito: o CORTEX captura o que descreve a pessoa e muda a conversa de venda; a ferramenta externa captura o que descreve a página.
Fica no CORTEX (tracking) |
Fica na ferramenta externa |
|---|---|
page_view, click, doc_read, video_progress, form_submit |
heatmap, profundidade de scroll, rage click, gravação de sessão, Core Web Vitals |
- Gravação de sessão não entra no CORTEX, e o motivo não é custo. A gravação captura o que a pessoa digita antes de submeter — inclusive o que ela apaga e nunca envia. Isso é PII entrando por um caminho que não é o do §7 (canal dedicado, cifrado, destino
identity): atravessa por baixo o desenho inteiro de PII, sem consentimento específico e sem que ninguém tenha decidido coletar aquilo. Usar a ferramenta externa, sim; trazer a gravação para dentro, não. - Métrica que descreve a página (Core Web Vitals, tempo de carregamento) não tem sujeito: não se correlaciona com lead nenhum e não precisa de chave (§20).
- O
trackingnão deixa de capturar o que já é dele porque a ferramenta externa também enxerga. O vocabulário de evento (§10) segue sendo a fonte de verdade do comportamento de negócio; a ferramenta externa não é consultada para decidir venda.
Consequência aceita: heatmap e gravação vivem fora, e analisar página exige abrir outra ferramenta. Em troca, nenhuma PII entra por caminho não desenhado.
20. Correlação com ferramenta externa: pseudônimo por ferramenta, cunhado no servidor (Obrigatório)
Correlacionar a sessão do CORTEX com a da ferramenta externa exige um dado comum. Esse dado não é o lead_id cru: entregue a terceiro, ele vira correlacionável fora do nosso domínio e cruzável entre fornecedores — dois vazamentos independentes passam a compor uma trilha só.
A chave é um pseudônimo derivado por ferramenta:
pseudo_<ferramenta> = HMAC(pepper_<ferramenta>, lead_id)
- Um
pepperdistinto por ferramenta. Estável dentro dela (a trilha lá continua fazendo sentido), inútil fora dela (não cruza com a de outro fornecedor), reversível por nós (o casamento de volta é local). - Cunhado no servidor. O
peppernunca vai para o browser: tag que calcula o HMAC no cliente publica o segredo, e o pseudônimo vira decoração. O servidor entrega o valor já pronto à página. person_idnunca vai a terceiro. Ele é o handle ligado a PII (identity); enviá-lo anula a fronteira do §2 e deecosystem.services.identity-pii-at-rest.- Só com
consent_state = granted(§5) — envio de identificador a terceiro é exatamente o que o nível base proíbe. Sem consentimento, a ferramenta externa roda sem tag de correlação: continua medindo a página, apenas não sabe de quem é a sessão. - Correlacionar, não importar. Guardar o pseudônimo e montar link para a fonte ("ver gravação desta sessão"), em vez de trazer o dado para dentro: zero duplicação de PII e sempre atualizado na origem — mesmo princípio de contexto rico puxado na fonte (ECOSYSTEM §5). Importar apenas agregado que muda decisão (ex.: sinal de engajamento para o
ai-sales), nunca o cru. - A etiqueta é client-side; puxar de volta é connector. A tag da ferramenta roda no browser e recebe o pseudônimo pronto — nenhum serviço do CORTEX chama o terceiro nesse caminho. Puxar agregado da API dele é trabalho de connector (§13: o
trackingnão fala com terceiro), nomeado por domínio e não por fornecedor (ecosystem.connectors.domain-naming). Enquanto a integração for só etiqueta e link, não existe connector — não criar um antes de haver o que importar.
Consequência aceita: o pseudônimo é opaco também para nós dentro da ferramenta — lá não há como buscar por pessoa, só chegar pela sessão que já sabemos qual é.
21. Campo de negócio do formulário: catálogo, não string livre (Obrigatório)
Um formulário de LP produz o sinal mais valioso que existe — "curso de interesse" é o que o ai-sales precisa para saber sobre o que conversar. Mas hoje esse campo não tem onde morar: o _pii_slice guarda só identificador (name, email, phone, documento) e descarta o resto — minimização correta, exigida por ecosystem.services.tracking-durable-capture — e a borda joga fora campo não declarado (§22). O dado some com 200 na resposta, sem erro.
Não basta abrir o metadata. events.metadata é jsonb livre e não cifrado, e POST /forms é endpoint público e não autenticado (a write_key roda no browser — pública por construção, §8). Aceitar ali qualquer chave deixaria qualquer formulário de terceiro, ou qualquer um com a write_key, despejar CPF e telefone numa coluna em claro — contornando o capture_inbox cifrado por outra porta, exatamente o controle que o _pii_slice existe para garantir.
A saída segue o padrão que o serviço já usa duas vezes (event_types §10, acquisition_param_types §10-bis): catálogo, nunca string livre.
- Nasce
tracking.business_metadata_keys, mesma forma:tenant_idnullable (NULL= global, preenchido = do tenant, sem poluir os demais),first_seen_at, colunas base. - Chave não catalogada é recusada (
422, com a chave no corpo), não ignorada. Cadastrar chave não exige deploy — é configuração, distribuída peloinstitutionalpor fato (ecosystem.services.institutional-config-distribution). first_seen_atpelo mesmo motivo do §10-bis: sem ele, "quantos pediram Direito em 2026?" não separa ninguém pediu de não estávamos perguntando — as três respostas de lá valem igual aqui.
A restrição que fecha o buraco de PII fica no cadastro, não no envio. A chave catalogada declara que não é identificador: nome que colida com o vocabulário de PII (cpf, email, telefone, documento, rg, passaporte e variações) é recusado ao catalogar. E o valor tem teto de tamanho — campo de negócio é rótulo, não texto livre; texto livre é onde a PII entra por acidente.
Risco que permanece (aceito, não resolvido): alguém pode catalogar observacao e a pessoa digitar o CPF lá dentro. Nenhuma regra técnica resolve isso — é o mesmo caso da gravação de sessão (§19). Registrado nas Fronteiras como risco conhecido e aceito.
22. A borda recusa o que não entende (Obrigatório)
Descartar em silêncio o campo não reconhecido (extra="ignore" na borda de ingestão) passa a ser proibido. Campo que a borda não conhece devolve 422 nomeando o campo.
O motivo não é purismo de contrato. Um campo de negócio descartado sem aviso custou meia hora de teste sem entender por que o dado não aparecia; em produção é uma campanha inteira coletando um campo que nunca chegou, com 200 em toda resposta e nada no banco. Silêncio na borda é o defeito mais caro que existe — só se descobre no dia em que alguém pede o relatório.
Contrapartida honesta: recusar campo não declarado torna a borda frágil a mudança de cliente — um SDK novo mandando um campo a mais quebra. É aceitável porque a borda é nossa e porque o catálogo do §21 é justamente o que permite acrescentar campo sem tocar no contrato. Esta regra depende do §21: sozinha, sem uma via legítima para campo novo, seria um freio ruim.
23. Preenchimento do formulário: forma, nunca conteúdo (Obrigatório)
Saber até onde a pessoa preencheu um formulário não exige o conteúdo — e gravá-lo reabriria o buraco de PII por outra porta.
Proibido gravar valor de campo de formulário em events.metadata — íntegro ou mascarado, truncado, hasheado ou ofuscado, e qualquer derivação reversível. Mascarar não salva: ma***@exemplo.com + 5544*****1111 + as iniciais do nome, na mesma linha com URL e timestamp, re-identifica — e a LGPD só tira do escopo o dado anonimizado, não o pseudonimizado (Art. 12). Seria a mesma PII que o capture_inbox guarda cifrada e apaga em segundos, só que em claro, num jsonb não cifrado, gravado por endpoint público, em tabela imutável e particionada — nunca purgada. Reforça o §21 por outro ângulo: o §21 recusa nomes de PII no cadastro; esta regra recusa o conteúdo, qualquer que seja o nome da chave.
No lugar, a chave reservada form_fields registra só a forma:
statepor campo, vocabulário fechado:filled|empty|invalid. Oinvalidé o mais valioso — diz onde o formulário trava.length_bucketopcional e em faixa, nunca número exato:1-5|6-20|20+. Faixa porque tamanho exato de CPF/telefone é sinal de identificação; faixa não é.- Nada além disso — nenhum valor, trecho ou hash.
form_fields é chave reservada de forma fixa (como source_ref), não catalogada pelo §21 (o catálogo é para rótulo configurável por tenant; configurável viraria depósito — ser fechada é o que garante que ninguém enfie valor ali). A borda valida a forma e recusa com 422 (§22) nome de campo desconhecido, state fora do vocabulário, ou atributo além desses dois.
24. O que não chegou também precisa de sinal (Obrigatório)
O form_submit só chega quando o envio dá certo; abandono no meio ou falha de POST /forms (rede, validação, aba fechada) não deixam registro — e "o formulário quebrou" fica indistinguível de "ninguém se interessou". Grave nas LPs legadas, onde o SDK é o único instrumento. Para saber do que não chegou, o sinal sai antes do submit — dois tipos novos no catálogo global do §10 (contrato: migration, versionados com o código, o SDK precisa saber emiti-los):
| tipo | quando | carrega |
|---|---|---|
form_progress |
um campo perde o foco (blur) |
form_fields do estado atual |
form_error |
o submit falhou | form_fields + error_code |
form_submit |
envio aceito | form_fields como confirmação |
error_codeé vocabulário fechado:network|validation|rejected|timeout.form_progressé coalescido, não um evento por tecla: dispara noblure manda o mapa inteiro (não o delta) — o último evento basta, e perder os intermediários não perde informação.form_errorusakeepalivepara sobreviver ao fechamento da aba; se a rede caiu, fica emlocalStoragee sai na próxima visita, com ooccurred_atoriginal (§9,clamp_occurred_at).client_event_idpor evento — reenvio deduplica (§9).- Consentimento: os três não carregam PII, então o nível base (
absent/denied) os aceita; nenhuma exceção ao §5. - Nenhum dos dois vira fato. Como
page_view/click(§13),form_progresseform_errornão vão ao core —form_progressé ainda mais volumoso. Ficam notracking_db, lidos na fonte pela API interna do §14; acrescentar consumidor é decisão nova. - Retenção:
form_progresspode ter retenção mais curta que a doform_submit(valor analítico e recente, não histórico) — o número não é fixado aqui; a tracking mede e propõe depois.
25. Recusa vira fila de revisão, nunca catalogação automática (Obrigatório)
O §21 exige chave catalogada e o §22 recusa (422) a que não estiver — juntas, deixam um problema operacional: ninguém sabe quais chaves existem no parque de dezenas de LPs, e cada campo não catalogado é um 422 que quem publicou a LP não vê. O dado não some na borda, mas some no processo.
Quando a borda recusa uma chave de metadata não catalogada, ela registra o NOME da chave como candidata (contagem, primeira/última vez, URLs onde apareceu):
- Só o NOME. Nunca o valor — ele é descartado como hoje. É isso que mantém a fila fora do alcance do problema que o §21 impede.
- A resposta ao chamador não muda: continua
422. Registrar não é aceitar. - O operador vê a fila e cataloga o que fizer sentido, pelo caminho do §21 (que aplica o CHECK de vocabulário de PII e o teto de tamanho no cadastro). Mesmo padrão da
merge_candidatesdoidentity: o automático recusa, mas não descarta a informação de que recusou.
Catalogação automática é proibida em qualquer forma — inclusive "após N ocorrências", "se o valor parecer inofensivo" ou "modo permissivo por um prazo". O §21 tem dois controles e os dois vivem no cadastro (CHECK de nome-PII + teto de tamanho); uma fila que cataloga sozinha contorna os dois por um caminho que parece inofensivo ("veio do tráfego real, é legítima") — e devolveria ao emissor da LP a decisão que a regra tirou dele. A fila registra; a pessoa cataloga.
Limites de construção (endpoint público alimenta a tabela): teto de chaves distintas por tenant (ao atingir, para de registrar nomes novos e só conta); nome truncado e com vocabulário de caracteres restrito; expiração de candidata não catalogada e sem uso — o que ninguém catalogou em meses era erro de digitação, não campo. Sem isso, a fila vira canal de escrita no banco a partir da internet.
Os três limites têm de existir; os valores vivem no template/config, não nesta regra (mesmo princípio do uniscale.git.coverage-layered §30.7 — número em doc envelhece, e estes só se calibram depois de ver tráfego real). O serviço registra os limites efetivos no arranque, para afrouxar o teto não ser silencioso — ex.: fila de descoberta: teto 200 chaves/tenant, nome até 60 [a-z0-9_], expira em 90 dias (valores iniciais, a medir e ajustar por PATCH).
Operação: a fila naturalmente mora no institutional (que cataloga, §21); enquanto não há tela, é consultável pela API interna (§14) e a catalogação é operação administrativa — mesmo estado da merge_candidates do identity.
26. A volta do card: o CRM é fonte de evento da trilha (Obrigatório)
O crm-connector (CRM_CONNECTOR.md §5/§6) devolve ao CORTEX o que acontece com o card no CRM. O tracking consome essa volta e passa a ter o CRM como fonte de evento, ao lado do gtag — sem nunca chamar a API do CRM (§13). A trilha continua dele: o bitrix é origem de um tipo de evento, não autoridade sobre o histórico.
- Ao nascer o card, o
trackingguarda o vínculo entre olead_ide a referência externa do card (consumindocrm.card.created, CRM §5). É esse vínculo que resolve a volta — sem ele, um evento que traz só a referência do card não tem como virar história de um lead. Otrackingé dono desse mapa porque o card nasceu de um lead dele. Como guardar é decisão do serviço; a regra é que o vínculo exista e seja mantido, e que seja o único caminho de resolução (nunca busca por PII). - Ao mover, chega só a referência do card e a etapa resultante crua (consumindo
crm.card.moved, CRM §6) — sem sujeito, sem etapa anterior, sem id de evento. Otracking, dono da história, faz o que o connector não pode (ele não tem o lead nem guarda estado): resolve olead_idpelo vínculo acima, deriva a etapa anterior do último marco de etapa daquele lead, e grava um marco de mudança de etapa (crm_stage_change) na trilha — um event_type da 3ª origem (§10), derivado de integração. - A etapa viaja crua; traduzir para o estado canônico é leitura. Converter a etapa do CRM para o significado de negócio é feito na leitura/relatório, contra o mapa do
institutional— não viaja no evento nem acopla a trilha ao mapa. - Idempotência por estado resultante, não por id de evento (o CRM não dá um): se a etapa resultante é igual à última conhecida daquele lead, é no-op. A mesma regra cobre, de uma vez, a reentrega do fato e as edições do card que não mexeram em etapa (o CRM emite em qualquer edição). O filtro "mudou mesmo?" mora em quem tem a história.
- Fica só na trilha, por ora. Como
page_view/click(§13/§14), o marco de etapa não vai ao core/sales; é consultável na fonte pela API interna (§14). Propagar ao sales é decisão nova, quando o loop de vendas existir. - Entrega segue a mensageria da plataforma (
uniscale.events.*): at-least-once, consumo idempotente, carta-morta — mesmo princípio do §15. Reprocessar a fila inteira dá o mesmo estado final, pela idempotência por estado resultante. - O que não volta, por ora: quem moveu o card (ator) e movimentos que não sejam mudança de etapa ficam fora do v1 (CRM §6) — não se supõe, entra como decisão nova.
Fronteiras a definir
Não inventar. Cada item entra como pacote próprio, validando contra esta fundação.
- Fusão de cards no Bitrix — consequência de §15 para o
crm-connector: dois cards para a mesma pessoa após uma fusão. Desenho no pacote do CRM. - Mecanismo de distribuição da config do institutional —
write_key, allowlist de origem e mapa de propriedades digitais (§8) dependem do mecanismo de distribuição, que ECOSYSTEM §5 registra como "a definir". - Gestão e rotação da chave de cifra do inbox (§6) — depende do plano de infra (KMS/vault), não decidido aqui.
- Parecer jurídico sobre as bases legais de §5 — a regra fixa a mecânica; a validação da base legal é do DPO.
- Connector de analytics de página — só existe quando houver agregado a importar (§20). Enquanto for etiqueta + link, não há connector; quando vier, é nomeado por domínio, não por fornecedor.
- Rotação do
pepperde correlação (§20) — rotacionar quebra o histórico dentro da ferramenta externa. A política fica no plano de infra, junto com a gestão de chaves de §6. - Chave de negócio de nome genérico (§21) — a restrição de nome bloqueia colisão com o vocabulário de PII, mas não impede catalogar
observacaoe a pessoa digitar o CPF lá dentro. Risco conhecido e aceito, não resolvido — mesmo caso da gravação de sessão (§19); nenhuma regra técnica o elimina.
Impactos em documentos irmãos
Ao promover este documento, atualizar também:
ECOSYSTEM.md§7 — a fronteira "Schemas detrackingeidentity" deixa de estar adiada: ambos passam a ter doc dedicado.ECOSYSTEM.md§4 — adicionar ponteiro paracortex-docs/services/TRACKING_SERVICE.mdno verbete dotracking-service, como já existe para payment/storage/institutional.ECOSYSTEM.md§3, §4 e §5 — o texto atual chama olead_idde "handle universal que todos os serviços passam a usar". Com operson_id(identity §1), olead_idé handle de origem de navegação, confinado ao par tracking/identity; o handle de pessoa é operson_id. Emenda necessária.ECOSYSTEM.md§6 — registrar as ferramentas de comportamento de página (Clarity, PageSpeed) na tabela de plataformas externas, com a fronteira de §19/§20.UNISCALE_ENGINEERING_STANDARDS.md§31.3 — registrar a exceção de §7 deste doc (exchange direct para PII, desvio do padrão topic).CRM_CONNECTOR.md§5/§6 — a recíproca da volta do card passa a estar escrita como regra dona aqui (§26): o impacto que o doc do CRM marcava como "emenda a fazer no pacote" está realizado. Ao promover, o CRM pode marcar o item de impacto emTRACKINGcomo fechado.IDENTITY_SERVICE.md— falta a recíproca paralela: oidentityconsumircrm.card.createdpara gravarperson ↔ cardemexternal_refsquandoperson_idestá presente (CRM §5). Emenda irmã, ainda a fazer — não bloqueia a trilha do §26 (que resolve porlead_id), mas fecha o par do §5.
CHANGELOG
| Versão | Data | Resumo |
|---|---|---|
| 0.10.0 | 2026-10-14 | Escreve a recíproca da volta do card (habilita o 1º corte de construção do crm-connector, §6 do CRM). §26 nova (tracking-crm-return): o tracking consome a volta do CRM e passa a tê-lo como fonte de evento, ao lado do gtag, sem nunca chamar a API do CRM — ao nascer o card guarda o vínculo lead_id ↔ referência externa do card (o único caminho de resolução, nunca por PII), e ao mover resolve o lead pelo vínculo, deriva a etapa anterior do último marco daquele lead e grava crm_stage_change na trilha; idempotência por estado resultante (etapa igual à última = no-op, cobre reentrega e edição que não mexeu em etapa); fica só na trilha, lido na fonte (§14); ator e movimentos que não sejam mudança de etapa ficam fora do v1. §10 (emenda): reconhece uma 3ª origem do vocabulário de evento — derivada de integração (nasce do consumo de fato, não da borda de captura nem de config de tenant; significado é contrato de plataforma, via de entrada é o consumo), categoria do crm_stage_change. Escrita no estilo conceito, não mecanismo (a decisão de como persistir/consumir é do repo de código). Impacto recíproco em CRM_CONNECTOR (realizado) e IDENTITY (par do §5 ainda a fazer). MINOR. |
| 0.9.0 | 2026-10-02 | §10-bis, a partir do complemento da sessão do tracking (caso real de campanha paga, 02/10). utm_id vira a 6ª coluna UTM — o conjunto fechado passa a source/medium/campaign/term/content/id, porque utm_id é a chave de reconciliação de campanha no Meta/GA4, tão padrão quanto utm_campaign (antes caía no vão: pulado pela regra dos UTM e pela regra do resto, sumia em silêncio). Fecha o vão do utm_* fora do conjunto: qualquer utm_* que não seja um dos seis é parâmetro de aquisição e vai para acquisition_params, nunca some. Teto é por destino, e estourar vira 422, não 500: coluna UTM varchar(255) e jsonb sem largura fixa têm tetos diferentes; valor acima do limite do seu destino é recusado na borda nomeando o campo (§22), nunca deixado estourar no INSERT (um fbclid real passa de 200 caracteres — teto único cortaria em produção). O valor dos tetos segue no template/config (§25/§30.7), calibrável — a sessão propôs 512 para acquisition_params, que é dela por ser config, não contrato. A retenção do click id já estava decidida na 0.8.0 (dado pessoal, apagado pelo esquecimento via §12, sem cifra nem expiração); o complemento não a reabre. MINOR. |
| 0.8.0 | 2026-09-30 | Fecha a pergunta da sessão do tracking sobre click ids (PERGUNTA_click_ids_retencao_e_teto), no §10-bis. Achado 1 — tetos na borda pública: POST /collect é público e não autenticado, e acquisition_params/acquisition_param_types não tinham teto no servidor (só no SDK, que qualquer um altera) — um cliente forjado enche o jsonb e cria linhas novas no catálogo. Passa a valer a mesma resposta do §25: teto de parâmetros distintos na sessão, teto de nomes distintos por tenant (ao atingir, o auto-cadastro para de registrar nomes novos e só conta), teto de tamanho de nome/valor e vocabulário de caracteres — validados no servidor; os três têm de existir, valores no template/config (§30.7/§25) e registrados no arranque. Achado 2 — click id é dado pessoal, decidido: gclid/fbclid/gbraid/wbraid/msclkid/ttclid são identificadores de clique (identificáveis por terceiro → dado pessoal de baixa sensibilidade), e o tratamento fica registrado: retidos para atribuição (inclusive LTV), não cifrados (são públicos e cifrar mata a análise), apagados pelo esquecimento do titular via a purga de sessões já existente no §12 (cadeia pessoa → lead_id → session) — este é o controle — e sem expiração própria, porque só re-identificam com acesso à conta da plataforma emissora, contra o qual expirar não protege. Sem regra nova além dos tetos; a classificação é nota de decisão. MINOR. |
| 0.7.1 | 2026-09-28 | Esclarece a §25 sem mudar a decisão: os três limites (teto por tenant, vocabulário do nome, expiração) têm de existir, mas os valores vivem no template/config, não na regra (precedente do §30.7 — número em doc envelhece, e estes só se calibram com tráfego real). Acrescenta a exigência de o serviço registrar os limites efetivos no arranque (afrouxar o teto não pode ser silencioso). Valores iniciais informados (200 chaves/tenant, nome ≤60 [a-z0-9_], expiração 90 dias) ficam como exemplo/a medir — o número medido entra por PATCH. PATCH. |
| 0.7.0 | 2026-09-28 | Ciclo de vida do formulário + fila de descoberta de chaves (emendas da sessão do tracking). §23 (tracking-form-completeness): proíbe valor de campo de formulário em events.metadata — íntegro ou mascarado/truncado/hasheado (pseudonimizado continua no escopo LGPD, Art. 12; metadata é jsonb não cifrado, endpoint público, tabela imutável nunca purgada). No lugar, a chave reservada form_fields grava só a forma: state (filled/empty/invalid) + length_bucket em faixa. Reforça o §21 pelo conteúdo (o §21 recusa nomes de PII; §23 recusa o conteúdo). §24 (tracking-form-lifecycle): form_progress (no blur, coalescido) e form_error (keepalive, error_code fechado) entram no catálogo global (§10, contrato); nenhum vira fato (como page_view/click, §13), ficam no tracking_db e são lidos pela API interna (§14); sem PII, nível base aceita (§5); retenção pode ser mais curta que a do form_submit (número medido depois). §25 (tracking-metadata-discovery): a recusa do §22 passa a registrar o NOME da chave como candidata (nunca o valor), virando fila de revisão — catalogação automática proibida em qualquer forma (contornaria os dois controles do §21); com teto por tenant, nome truncado/restrito e expiração; a pessoa cataloga pelo §21. §11: entra tracking.metadata_key_candidates. MINOR. |
| 0.6.0 | 2026-09-24 | Fecha os itens devolvidos pela sessão de identity/docs. §8 (emenda): a write_key passa a ser por propriedade digital, não por tenant — o servidor resolve tenant_id e digital_property_id da chave; o source_ref cai a sub-referência declarada (qual formulário), sem valer como identidade; allowed_origins é da propriedade. Motivos: atribuição verificada (tag copiada entre LPs sem trocar source_ref não erra mais), allowlist deixa de ser frouxa, e raio de revogação contido (chave por tenant derrubaria todas as LPs de uma vez). Casa com ecosystem.services.institutional-tenant-registry. §10 (esclarecimento), estendido aos três catálogos: a coluna tenant_id é a fronteira de propriedade — global (NULL) = contrato do tracking (migration, com o código), por-tenant = config do institutional (fato); vale igual para event_types, acquisition_param_types (§10-bis) e business_metadata_keys (§21). §8-bis (nova, tracking-key-rotation): rotação exige duas chaves válidas (valid_until no lugar de is_active no escuro) e telemetria de uso (sessions.ingest_key_id) — não se revoga sem consultar. §11: ingest_keys ganha digital_property_id e valid_until. MINOR. |
| 0.5.0 | 2026-09-11 | Fecha a lacuna do campo de negócio do formulário (defeito de teste 2026-09-10: curso_de_interesse sumia — descartado pelo _pii_slice por não ser PII, e pelo extra=ignore da borda, com 200 nas duas). Acrescenta §21 (tracking-business-metadata): campo de negócio não-PII vive em events.metadata, mas validado contra o catálogo tracking.business_metadata_keys — mesma forma de event_types/acquisition_param_types (tenant_id nullable, first_seen_at datado, sem deploy, distribuído pelo institutional). Chave não catalogada é recusada (422), não ignorada. O buraco de PII (metadata livre e não cifrado num endpoint público) é fechado no cadastro: nome que colida com vocabulário de identificador (cpf, email, telefone, documento…) é recusado ao catalogar, e o valor tem teto de tamanho (rótulo, não texto livre). Acrescenta §22 (tracking-strict-edge): extra=ignore na borda passa a proibido — campo não reconhecido devolve 422 nomeando o campo; silêncio na borda é o defeito que só se descobre no dia do relatório. §22 depende do §21: o catálogo é o que permite acrescentar campo sem mexer no contrato. §11: entra tracking.business_metadata_keys. Nova fronteira: chave de nome genérico (observacao) ainda admite PII digitada — risco conhecido e aceito, como a gravação de sessão do §19. MINOR. |
| 0.4.0 | 2026-09-10 | Fecha a fronteira entre comportamento de pessoa e comportamento de página. Acrescenta §19 (tracking-page-analytics-boundary): o CORTEX captura o que descreve a pessoa e muda a conversa de venda (page_view, click, doc_read, video_progress, form_submit); heatmap, scroll, rage click, gravação de sessão e Core Web Vitals ficam na ferramenta externa. Gravação de sessão não entra — e o motivo não é custo: ela captura o que a pessoa digita antes de submeter, inclusive o que apaga e nunca envia, o que faz PII entrar por um caminho que não é o canal dedicado do §7, sem consentimento específico e sem decisão de coletar. Acrescenta §20 (tracking-third-party-pseudonym): a chave de correlação é HMAC(pepper_<ferramenta>, lead_id), com pepper distinto por ferramenta e cunhado no servidor (pepper no browser publica o segredo e torna o pseudônimo decorativo); person_id nunca vai a terceiro; envio só com consent_state = granted, e sem consentimento a ferramenta roda sem tag de correlação. Fixa correlacionar, não importar: guardar o pseudônimo e montar link para a fonte, importando apenas agregado que mude decisão. A etiqueta é client-side; puxar da API do terceiro é trabalho de connector (§13), que não se cria antes de haver o que importar. §5: a linha de envio a terceiro passa a cobrir o pseudônimo de correlação, não só a conversão. MINOR. |
| 0.3.0 | 2026-09-10 | Acrescenta §10-bis (tracking-acquisition-params): parâmetro de aquisição fora dos UTMs vive em sessions.acquisition_params (jsonb + GIN) e não exige DDL; a otimização é índice de expressão, só quando o parâmetro virar quente. E o catálogo acquisition_param_types grava first_seen_at automaticamente, porque sem ele uma consulta futura não separa ausência conhecida de desconhecido e conclui que o canal não trouxe ninguém quando apenas não era etiquetado — problema que existiria igual com coluna nova ou banco de documentos. Acrescenta §18 (tracking-lead-sharing): o lead ganha sharing_state (single/shared), detectado ao vincular um segundo person_id distinto. Em shared, consulta por pessoa não devolve comportamento daquele navegador — o dado continua consultável por lead_id, mas o serviço passa a dizer "não sei de quem foi" em vez de herdar o comportamento para todas as pessoas vinculadas. O motivo não é precisão: atribuir faz a ficha de uma pessoa exibir o que outra navegou, o que é vazamento entre duas pessoas que só dividem um teclado, e faz a IA conduzir a conversa com o interesse errado. O vínculo N:N continua gravado como fato (auditoria, fraude); o que se proíbe é a leitura atravessá-lo. §17 passa a citar a regra. MINOR. |
| 0.2.2 | 2026-09-10 | Reconcilia a regra com a implementação, sem mudar decisão de arquitetura. §11: entra tracking.ingest_keys — réplica local da config do institutional, exigida por §8 mas ausente da lista de tabelas; e a dispensa de autoria nas tabelas técnicas passa a ser declarada explicitamente, ao lado da dispensa de soft delete que já estava (em events seriam dois UUID constantes por linha numa tabela de bilhões, apontando sempre para a mesma conta de serviço). §15: nomeia a fila tracking-identity__person-facts por onde os fatos do identity voltam, com ack manual, DLQ e idempotência por source_event_id — o comportamento já era regra, faltava o endereço. PATCH. |
| 0.2.1 | 2026-09-02 | Editorial de governança, sem mudança de regra ativa: restaura o slug ecosystem.services.tracking-merge-alias como depreciado, com superseded-by=ecosystem.services.tracking-lead-identities (§15-bis). Na v0.2.0 a seção foi substituída sem deixar marcador, e o slug — que já estava publicado — passou a responder unknown rule slug, violando doc.slug.stability (slug publicado é imutável e nunca reusado; regra substituída marca o antigo, não o apaga). Nenhuma regra ativa muda. PATCH. |
| 0.2.0 | 2026-08-31 | Ajusta o modelo de identidade à decisão de arquitetura: o person_id é cunhado pelo identity na identificação, e o lead_id passa a ser explicitamente identificador de navegador, não de pessoa (§1). Em consequência: tracking.lead_aliases (lead_id canônico) é substituída por tracking.lead_identities (lead_id ↔ person_id) e lead_merges por lead_identity_journal (§11); §15 é reescrita — o tracking grava o vínculo publicado pelo identity em vez de eleger um lead_id vencedor, com cardinalidade 1 pessoa : N navegadores e a exceção do computador compartilhado reconhecida e não modelada como regra; §17 deixa de reescrever o cookie (o lead_id é estável por 400 dias, que é o que permite reconhecer a visita de semanas atrás); §12 passa a executar purga por consumo de identity.person.erased; §13 fixa que todo fato carrega person_id quando conhecido. Adiciona à seção de impactos a emenda ao ECOSYSTEM §3/§4/§5 sobre "handle universal". MINOR. |
| 0.1.0 | 2026-08-31 | Documento inicial. Promove a @rule endereçável (área services, slugs ecosystem.services.tracking-*) o desenho do tracking-service, que ECOSYSTEM §7 mantinha adiado. Fixa: berço do lead_id em UUIDv7 (§1); divisão de propriedade com o identity (§2); borda exposta como relaxe delimitado (§3); nascimento não-web pelo mesmo berço (§4); consentimento em dois níveis com consent_state viajando no fato, e proibição de fingerprint (§5); parâmetros da captura durável que a doc deixou em aberto — backoff, teto de 24h, apagamento imediato pós-entrega, AES-256-GCM (§6); entrega de PII em exchange direct dedicado, desvio consciente do topic, a registrar como exceção (§7); tenant resolvido da write_key no servidor (§8); idempotência por client_event_id (§9); vocabulário de evento em lookup extensível sem deploy (§10); modelo de dados com dispensa explícita de soft delete por tabela (§11); retenção diferenciada por consentimento e purga por titular (§12); fatos publicados e proibição de falar com terceiros direto (§13); core recebe marco e ponteiro, com API de leitura interna obrigatória (§14); vínculo de identidade sem reescrita de histórico (§15); nunca cross-tenant (§16); leitura resolve a pessoa (§17). |