هذه المقالة غير متاحة بلغتك بعد، لذا تقرأ النسخة الإنجليزية الأصلية.
Context
Advertisers lose budget to bots, VPN, proxy and data-center traffic. A report that arrives weeks later only documents the loss. ClickFortify scores every paid click on pages that carry its script, and acts on the bad ones while the campaign is still spending: flagged IPs are excluded in Google Ads, and suspicious conversions are flagged or filtered before they reach Meta.
I am the lead architect.
Constraints
- Every advertiser is a workspace. We call them spaces. Accounts, rules and data are scoped per space and must never leak into each other.
- Detection runs on live traffic, continuously. Collection has to stay fast even when something downstream is slow.
- The action happens in someone else's system. An exclusion only counts once it exists in the advertiser's Google Ads or Meta account, and those APIs have quotas, limits and their own failure modes.
Architecture
The backend is a set of NestJS services: a tracker that collects clicks, a worker that scores them and syncs exclusions, a webhook service for ad-platform and billing events, and the API behind the Next.js dashboard. They hand work to each other through Redis-backed Bull and BullMQ queues, so each one scales on its own and a slow ad-platform API turns into a backlog instead of an outage.
The worker is split into processors by job: ad clicks, behaviour, device and threat signals, bot tells, Google Ads sync, Meta Ads, reconciliation, billing and email. Scoring combines device, network and behaviour signals, with MaxMind data for IP intelligence. Everything lands in PostgreSQL through Prisma. Spaces share one database and are separated by scope, not by separate databases.
The illustration below is a simplified, interactive walk through one click, from arrival to exclusion. The scoring in it is illustrative, not the production model.
تشريح نقرة محظورة
نموذج مبسّط لأغراض التوضيح فقط، وليس القواعد أو الأوزان المعتمدة في بيئة الإنتاج.
درجة المخاطر
0 · مسموح بها
ما الذي يحدث لهذه النقرة
- يسجّل Tracker النقرة ويستجيب فورًا.
- توضع النقرة في قائمة انتظار، فلا يؤدي أي ارتفاع مفاجئ في الزيارات إلى إبطاء عملية الجمع.
- تقوم عملية معالجة (worker) بتقييمها بناءً على الإشارات أعلاه.
- يُخزَّن القرار مع الأسباب التي استند إليها.
- لا يُتخذ أي إجراء. تُحتسب النقرة زيارة حقيقية.
It runs on Kubernetes on GCP and deploys through ArgoCD. Prisma migrations run as an ArgoCD PreSync hook, so the schema is migrated before any new pod starts.
Decisions and alternatives I rejected
No silent changes in a customer's ad account
During the Meta connect work, the suggestion was to create a Meta Pixel automatically when the customer didn't have one. I rejected it. ClickFortify never creates anything in a customer's external account in the background. Any flow that would provision something shows a review step first: an editable name, the account or business it will be created under, what we will do with it, and a Create button the customer presses. Defaults are fine. Silent execution is not: it takes away the customer's say over naming and placement.
Three repositories for GitOps, not one
The original plan kept the Kubernetes manifests inside the application monorepo. I split it into three repositories instead: the app, the manifests ArgoCD deploys, and the platform infrastructure. ArgoCD re-clones the repository on every refresh, and the monorepo is large. The platform also has its own lifecycle and reviewers, and the app's CI has no access to the platform repository at all.
Migrations that fail closed
The migration job runs the image being deployed and is not retried. The alternative, letting Kubernetes retry a failed migration, risks running it again over a half-applied schema. With the PreSync hook, a failed migration aborts the whole sync and keeps its logs, and the pods never run against a schema they don't expect.
What went wrong
The tracker saw the wrong client IP
The tracker sits behind a CDN and a reverse proxy, and sometimes behind Google's own relays as well. We worked out the visitor's IP by counting proxy hops. When the hop count was too low, clicks were recorded with the CDN's edge address and scored as bad. When we raised it, Google's relay became the "visitor", the bot filter treated it as a known bot, and the click was dropped with only a debug log. Hop counting breaks every time the topology changes, so we stopped depending on it and verified the fix on live traffic, with visitor IPs back in local ISP ranges.
A related lesson: a dropped click needs a record. Clicks for an inactive space used to vanish with no error. They are now stored as ignored, with the reason.
Outcome
ClickFortify syncs IP exclusions back to advertisers' ad accounts automatically. Releases go out through GitOps on Kubernetes, and a failed migration stops a release before it reaches the pods.
The move from a single VM to the cluster was a staged cutover. The new stack ran behind a parallel set of stage hostnames, was verified there, and then the production hostnames were repointed in one flip. No data moved, because both sides used the same databases. That also meant the worker stayed at zero replicas on the staging side, since its scheduled jobs would otherwise have run twice against production data. After the flip, live tracker traffic showed real visitor IPs.
The lesson I keep coming back to: detection is the easy half. The hard half is making sure the exclusion actually exists in someone else's system, and noticing quickly when it doesn't.