conversion: Trichter misst Verwaltungsseite, Anforderung und Firma bei der Einlösung
Roadmap 6.16. Bisher stand jede Einlösung ohne Firma im Trichter (Stufe "Magic Link eingelöst" immer 0 Firmen), zwischen Mail-Klick und Einlösung fehlten "Verwaltungsseite geöffnet" und "Anmeldelink angefordert", und die Erstbuchung stand immer auf organic. Die Leiter hat jetzt sieben Stufen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f5e3f1d1fe
commit
e8cf684f02
10 changed files with 211 additions and 52 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Enums\ConversionEventType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MagicLink;
|
||||
use App\Models\PressRelease;
|
||||
|
|
@ -75,13 +74,10 @@ class MagicLinkConsumeController extends Controller
|
|||
return redirect()->route('login')->with('status', __('The magic login link has expired or was already used.'));
|
||||
}
|
||||
|
||||
// Stufe 2 der Konversionsleiter. Die Herkunft kommt vom Token, nicht
|
||||
// Stufe 4 der Konversionsleiter. Die Herkunft kommt vom Token, nicht
|
||||
// aus der Session — die ist beim Weg über Mail und Domainwechsel
|
||||
// längst gerissen (Roadmap 0.4).
|
||||
$this->recorder->record(
|
||||
ConversionEventType::MagicLinkConsumed,
|
||||
$this->recorder->sourceOf($magicLink),
|
||||
);
|
||||
$this->recorder->recordMagicLinkConsumed($magicLink);
|
||||
|
||||
$user = $magicLink->user;
|
||||
|
||||
|
|
@ -123,10 +119,7 @@ class MagicLinkConsumeController extends Controller
|
|||
]);
|
||||
|
||||
if ($claimed > 0) {
|
||||
$this->recorder->record(
|
||||
ConversionEventType::MagicLinkConsumed,
|
||||
$this->recorder->sourceOf($magicLink),
|
||||
);
|
||||
$this->recorder->recordMagicLinkConsumed($magicLink);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Enums\ConversionEventType;
|
||||
use App\Enums\ConversionSource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
|
|
@ -9,6 +10,7 @@ use App\Models\PressRelease;
|
|||
use App\Scopes\PortalScope;
|
||||
use App\Services\Company\MergedNewsroomLookup;
|
||||
use App\Services\Company\NewsroomClaimService;
|
||||
use App\Services\Conversion\ConversionRecorder;
|
||||
use App\Support\WebPortalContext;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
|
@ -29,9 +31,10 @@ class NewsroomClaimController extends Controller
|
|||
public function __construct(
|
||||
private readonly NewsroomClaimService $claims,
|
||||
private readonly MergedNewsroomLookup $mergedNewsrooms,
|
||||
private readonly ConversionRecorder $recorder,
|
||||
) {}
|
||||
|
||||
public function show(string $edition, string $slug): View|Response|RedirectResponse
|
||||
public function show(Request $request, string $edition, string $slug): View|Response|RedirectResponse
|
||||
{
|
||||
$context = WebPortalContext::fromRequest();
|
||||
$company = $this->claims->findClaimable($context->portalValues, $slug);
|
||||
|
|
@ -40,6 +43,8 @@ class NewsroomClaimController extends Controller
|
|||
return $this->resolveMissing($context, $edition, $slug);
|
||||
}
|
||||
|
||||
$this->recorder->recordManagePageOpened($request, $company, $this->sourceOf($request), $this->landingPressRelease($request, $company));
|
||||
|
||||
return view('web.newsroom-claim', [
|
||||
'company' => $company,
|
||||
'recipients' => $this->claims->maskedRecipients($company),
|
||||
|
|
@ -70,7 +75,8 @@ class NewsroomClaimController extends Controller
|
|||
|
||||
// Enumeration-sicher: dieselbe Meldung, ob ein Konto deaktiviert ist
|
||||
// oder nicht — die Adresse ist ohnehin nur maskiert sichtbar.
|
||||
$sent = $this->claims->request($company, $request->ip(), $this->sourceOf($request), $this->landingPressReleaseId($request, $company));
|
||||
$landing = $this->landingPressRelease($request, $company);
|
||||
$sent = $this->claims->request($company, $request->ip(), $this->sourceOf($request), $landing?->getKey());
|
||||
|
||||
// Der Firmenzähler zählt nur tatsächlich versendete Links — sonst
|
||||
// sperrten Dritte den Inhaber mit Fehlversuchen 24 h aus
|
||||
|
|
@ -78,6 +84,10 @@ class NewsroomClaimController extends Controller
|
|||
// Drossel oben.
|
||||
if ($sent > 0) {
|
||||
RateLimiter::hit($this->companyKey($company->getKey()), 86400);
|
||||
|
||||
// Stufe 3 der Konversionsleiter (Roadmap 6.16): nur, wenn
|
||||
// wirklich ein Link verschickt wurde.
|
||||
$this->recorder->record(ConversionEventType::AccessRequested, $this->sourceOf($request), $company, $landing);
|
||||
}
|
||||
|
||||
return back()->with('newsroom-claim-status', __('static.newsroom_claim.flash_sent'));
|
||||
|
|
@ -88,7 +98,7 @@ class NewsroomClaimController extends Controller
|
|||
* Link), landet er nach dem Login auf genau dieser Meldung samt Aufrufen
|
||||
* (Roadmap 1c.2) — sofern sie wirklich zu dieser Firma gehört.
|
||||
*/
|
||||
private function landingPressReleaseId(Request $request, Company $company): ?int
|
||||
private function landingPressRelease(Request $request, Company $company): ?PressRelease
|
||||
{
|
||||
$id = (int) $request->input('pr', 0);
|
||||
|
||||
|
|
@ -96,7 +106,7 @@ class NewsroomClaimController extends Controller
|
|||
return null;
|
||||
}
|
||||
|
||||
return PressRelease::query()->withoutGlobalScope(PortalScope::class)->whereKey($id)->where('company_id', $company->getKey())->exists() ? $id : null;
|
||||
return PressRelease::query()->withoutGlobalScope(PortalScope::class)->whereKey($id)->where('company_id', $company->getKey())->first();
|
||||
}
|
||||
|
||||
private function resolveMissing(WebPortalContext $context, string $edition, string $slug): Response|RedirectResponse
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use App\Enums\ConversionSource;
|
|||
use App\Models\PressRelease;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\Conversion\ConversionRecorder;
|
||||
use App\Support\CrawlerIdentifier;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
|
@ -49,7 +48,7 @@ class RecordMailLinkClick
|
|||
// Nur echte Aufrufe. Mailclients und Sicherheitsscanner holen Links
|
||||
// vorab per HEAD oder mit eigener Kennung; das wäre kein Klick,
|
||||
// sondern eine Zustellung (Umsetzungsaudit 29.09.2026, R-08).
|
||||
if (! $request->isMethod('GET') || $this->isScanner($request->userAgent())) {
|
||||
if (! $request->isMethod('GET') || $this->recorder->isAutomatedVisit($request->userAgent())) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
|
@ -81,23 +80,6 @@ class RecordMailLinkClick
|
|||
return $next($request);
|
||||
}
|
||||
|
||||
private function isScanner(?string $userAgent): bool
|
||||
{
|
||||
$userAgent = mb_strtolower(trim((string) $userAgent));
|
||||
|
||||
if ($userAgent === '' || CrawlerIdentifier::identify($userAgent) !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ((array) config('conversion.mail_clicks.scanner_agents', []) as $pattern) {
|
||||
if (str_contains($userAgent, mb_strtolower((string) $pattern))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope-frei aufgelöst: Der Klick landet je nach Link auf dem Hub oder auf
|
||||
* einem der beiden Spokes, der Portal-Scope würde die Meldung dort
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue