* feat: capturar origem de marketing do briefing de contato (MAN-105) Adiciona tracking simples da origem (UTM ou referrer, primeiro-toque vence) para satisfazer WEB-05: a origem viaja apenas com o e-mail interno do briefing, sem Lead model, tabela ou superfície de CRM (Fase 2 fora de escopo). Middleware no grupo "web" cobre só as rotas públicas (o painel Filament tem sua própria stack de middleware); lógica de captura/sanitização isolada em App\Domain\Contact para não poluir controller nem view. Fail-open por construção: qualquer falha ao capturar ou ler a origem é logada sem dados pessoais e a submissão segue normalmente. Respeita LGPD (§12.5): nada além da origem é guardado, valores são truncados para não inflar a sessão, e um referrer apontando para o próprio host é descartado por não ser sinal de marketing. Valores não confiáveis (query string, referrer) são sanitizados antes de entrar na sessão e no e-mail em HTML. Co-Authored-By: Claude noreply@anthropic.com AI-Assisted: yes AI-Tool: claude-code * fix(contato): capturar apenas a origem do referrer, não a URL completa O fallback de referrer em MarketingOrigin::capture() armazenava a URL inteira (incluindo query string e fragmento), não apenas o site de origem. Um link de campanha de e-mail personalizado (ex.: ?email=...&subscriber_id=...) chegava intacto à sessão (persistida em banco via SESSION_DRIVER=database) e ao corpo do e-mail de briefing lido por um humano — indo além do que SPEC.md WEB-05 pede ("origem de marketing") e do §12.5 ("coletar apenas dados necessários"). Agora o referrer é reduzido a esquema+host antes de sanitizar, descartando path, query string e userinfo. Co-Authored-By: Claude noreply@anthropic.com AI-Assisted: yes AI-Tool: claude-code * test(contato): cobrir first-touch quando a navegação seguinte traz UTM concorrente test_first_touch_wins_when_later_navigation_has_no_utm não exercitava de fato a guarda "primeira captura vence" em CaptureMarketingOrigin — as duas navegações seguintes não carregavam nenhum sinal (nem UTM, nem referrer), então MarketingOrigin::capture() já retornava [] e o early-return por ausência de sinal (não a guarda de sessão) é quem impedia a sobrescrita. Confirmado removendo a guarda: a suíte continuava passando. Este teste novo usa uma segunda navegação com UTM diferente (utm_source=facebook), o que só passa se a guarda de sessão estiver ativa — mutação verificada manualmente antes de escrever a asserção. Co-Authored-By: Claude noreply@anthropic.com AI-Assisted: yes AI-Tool: claude-code * fix(contato): não capturar origem em sitemap.xml e robots.txt O middleware estava anexado ao grupo `web` inteiro, que também cobre `/sitemap.xml` e `/robots.txt`. Um crawler chegando em `/sitemap.xml?utm_source=...` consumia o slot de primeiro toque com tráfego que nunca vai enviar um briefing, e ainda iniciava sessão em uma rota técnica. As duas rotas passam a ser ignoradas por nome, com teste cobrindo que elas não gravam nada na sessão e que uma visita humana seguinte ainda é capturada normalmente. Co-Authored-By: Claude noreply@anthropic.com AI-Assisted: yes AI-Tool: claude-code --------- Co-authored-by: manoel.neto <manoel.neto@creditas.com>
31 lines
1.1 KiB
PHP
31 lines
1.1 KiB
PHP
<?php
|
|
|
|
use App\Http\Middleware\CaptureMarketingOrigin;
|
|
use Illuminate\Foundation\Application;
|
|
use Illuminate\Foundation\Configuration\Exceptions;
|
|
use Illuminate\Foundation\Configuration\Middleware;
|
|
use Illuminate\Http\Request;
|
|
|
|
return Application::configure(basePath: dirname(__DIR__))
|
|
->withRouting(
|
|
web: __DIR__.'/../routes/web.php',
|
|
commands: __DIR__.'/../routes/console.php',
|
|
health: '/up',
|
|
)
|
|
->withMiddleware(function (Middleware $middleware): void {
|
|
// Trust Traefik/Dokploy (and local reverse proxies) for X-Forwarded-* headers.
|
|
$middleware->trustProxies(at: '*');
|
|
|
|
// Public-site request spine only (routes/web.php uses the "web"
|
|
// group; the Filament admin panel defines its own middleware
|
|
// stack in AdminPanelProvider and never touches this group).
|
|
$middleware->web(append: [
|
|
CaptureMarketingOrigin::class,
|
|
]);
|
|
})
|
|
->withExceptions(function (Exceptions $exceptions): void {
|
|
$exceptions->shouldRenderJsonWhen(
|
|
fn (Request $request) => $request->is('api/*'),
|
|
);
|
|
})->create();
|